[
  {
    "path": ".github/FUNDING.yml",
    "content": "open_collective: sagernet\nliberapay: nekohasekai\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\r\n\r\non:\r\n  push:\r\n    tags:\r\n      - 'v*'\r\n\r\njobs:\r\n  libcore:\r\n    name: Native Build (LibCore)\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - name: Checkout\r\n        uses: actions/checkout@v2\r\n      - name: Fetch Status\r\n        run: git submodule status library/core > libcore_status\r\n      - name: LibCore Cache\r\n        id: cache\r\n        uses: actions/cache@v2\r\n        with:\r\n          path: |\r\n            app/libs/libcore.aar\r\n          key: ${{ hashFiles('bin/lib/core/*', 'libcore_status') }}\r\n      - name: Install Golang\r\n        uses: actions/setup-go@v2\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        with:\r\n          go-version: 1.17.1\r\n      - name: Native Build\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        run: ./run lib core\r\n  shadowsocks:\r\n    name: Native Build (shadowsocks-rust)\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - name: Checkout\r\n        uses: actions/checkout@v2\r\n      - name: Fetch Status\r\n        run: git submodule status 'library/shadowsocks/*' > shadowsocks_status\r\n      - name: Shadowsocks Cache\r\n        id: cache\r\n        uses: actions/cache@v2\r\n        with:\r\n          path: |\r\n            app/libs/shadowsocks.aar\r\n          key: ${{ hashFiles('library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\r\n      - name: Install Rust\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        run: ./run init action shadowsocks\r\n      - name: Gradle cache\r\n        uses: actions/cache@v2\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        with:\r\n          path: ~/.gradle\r\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\r\n      - name: Native Build\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        env:\r\n          BUILD_PLUGIN: none\r\n        run: |\r\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\r\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\r\n          ./run init action library\r\n          ./run lib shadowsocks\r\n  shadowsocks_libev:\r\n    name: Native Build (shadowsocks-libev)\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - name: Checkout\r\n        uses: actions/checkout@v2\r\n      - name: Fetch Status\r\n        run: git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\r\n      - name: shadowsocks-libev Cache\r\n        id: cache\r\n        uses: actions/cache@v2\r\n        with:\r\n          path: |\r\n            app/libs/shadowsocks-libev.aar\r\n          key: ${{ hashFiles('library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\r\n      - name: Gradle cache\r\n        uses: actions/cache@v2\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        with:\r\n          path: ~/.gradle\r\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\r\n      - name: Native Build\r\n        if: steps.cache.outputs.cache-hit != 'true'\r\n        env:\r\n          BUILD_PLUGIN: none\r\n        run: |\r\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\r\n          ./run init action library\r\n          ./run lib shadowsocks_libev\r\n  build:\r\n    name: Gradle Build\r\n    runs-on: ubuntu-latest\r\n    needs:\r\n      - libcore\r\n      - shadowsocks\r\n      - shadowsocks_libev\r\n    steps:\r\n      - name: Checkout\r\n        uses: actions/checkout@v2\r\n      - name: Fetch Status\r\n        run: |\r\n          git submodule status 'library/shadowsocks/*' > shadowsocks_status\r\n          git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\r\n          git submodule status library/core > libcore_status\r\n      - name: LibCore Cache\r\n        uses: actions/cache@v2\r\n        with:\r\n          path: |\r\n            app/libs/libcore.aar\r\n          key: ${{ hashFiles('bin/lib/core/*', 'libcore_status') }}\r\n      - name: Shadowsocks Cache\r\n        uses: actions/cache@v2\r\n        with:\r\n          path: |\r\n            app/libs/shadowsocks.aar\r\n          key: ${{ hashFiles('library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\r\n      - name: Shadowsocks (libev) Cache\r\n        uses: actions/cache@v2\r\n        with:\r\n          path: |\r\n            app/libs/shadowsocks-libev.aar\r\n          key: ${{ hashFiles('library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\r\n      - name: Debug Build\r\n        run: |\r\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\r\n          mkdir -p ~/.android && cp ./bin/debug.keystore ~/.android/debug.keystore\r\n          tree app/libs/\r\n          ./run init action library\r\n          ./gradlew app:assembleOssDebug\r\n          bash ./bin/re.sh\r\n          tree app/build/outputs/apk/\r\n      - name: update\r\n        uses: softprops/action-gh-release@v1\r\n        if: startsWith(github.ref, 'refs/tags/')\r\n        with:\r\n          files: |\r\n            app/libs/libcore.aar\r\n            app/libs/shadowsocks.aar\r\n            app/libs/shadowsocks-libev.aar\r\n            app/build/outputs/apk/oss/debug/AX-arm64-v8a-debug.apk\r\n            app/build/outputs/apk/oss/debug/AX-armeabi-v7a-debug.apk\r\n            app/build/outputs/apk/oss/debug/AX-x86-debug.apk\r\n            app/build/outputs/apk/oss/debug/AX-x86_64-debug.apk\r\n\r\n"
  },
  {
    "path": ".github/workflows/debug.yml",
    "content": "name: Debug build\n\non:\n  push:\n    branches:\n      - dev\n    paths-ignore:\n      - '**.md'\n      - '.github/**'\n      - '!.github/workflows/debug.yml'\n  pull_request:\n    branches:\n      - dev\n\njobs:\n  libcore:\n    name: Native Build (LibCore)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: git submodule status library/core > libcore_status\n      - name: LibCore Cache\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/libcore.aar\n          key: ${{ hashFiles('.github/workflows/*', 'bin/lib/core/*', 'libcore_status') }}\n      - name: Install Golang\n        uses: actions/setup-go@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          go-version: 1.17.1\n      - name: Native Build\n        if: steps.cache.outputs.cache-hit != 'true'\n        run: ./run lib core\n  shadowsocks:\n    name: Native Build (shadowsocks-rust)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: git submodule status 'library/shadowsocks/*' > shadowsocks_status\n      - name: Shadowsocks Cache\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\n      - name: Install Rust\n        if: steps.cache.outputs.cache-hit != 'true'\n        run: ./run init action shadowsocks\n      - name: Gradle cache\n        uses: actions/cache@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          path: ~/.gradle\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Native Build\n        if: steps.cache.outputs.cache-hit != 'true'\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          ./run init action library\n          ./run lib shadowsocks\n  shadowsocks_libev:\n    name: Native Build (shadowsocks-libev)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\n      - name: shadowsocks-libev Cache\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks-libev.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\n      - name: Gradle cache\n        uses: actions/cache@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          path: ~/.gradle\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Native Build\n        if: steps.cache.outputs.cache-hit != 'true'\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          ./run init action library\n          ./run lib shadowsocks_libev\n  Lint:\n    name: Android Lint\n    runs-on: ubuntu-latest\n    needs:\n      - libcore\n      - shadowsocks\n      - shadowsocks_libev\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: |\n          git submodule status 'library/shadowsocks/*' > shadowsocks_status\n          git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\n          git submodule status library/core > libcore_status\n      - name: LibCore Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/libcore.aar\n          key: ${{ hashFiles('.github/workflows/*', 'bin/lib/core/*', 'libcore_status') }}\n      - name: Shadowsocks Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\n      - name: Shadowsocks (libev) Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks-libev.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\n      - name: Gradle cache\n        uses: actions/cache@v2\n        with:\n          path: ~/.gradle\n          key: gradle-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Android Lint\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          ./run init action library\n          ./run lint"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Build\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Release Tag'\n        required: true\n      upload:\n        description: 'Upload: If want ignore'\n        required: false\n      publish:\n        description: 'Publish: If want ignore'\n        required: false\n      play:\n        description: 'Play: If want ignore'\n        required: false\njobs:\n  check:\n    name: Check Access\n    runs-on: ubuntu-latest\n    steps:\n      - name: \"Check access\"\n        uses: \"lannonbr/repo-permission-check-action@2.0.0\"\n        with:\n          permission: \"write\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  libcore:\n    name: Native Build (LibCore)\n    runs-on: ubuntu-latest\n    needs: check\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: git submodule status library/core > libcore_status\n      - name: LibCore Cache\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/libcore.aar\n          key: ${{ hashFiles('.github/workflows/*', 'bin/lib/core/*', 'libcore_status') }}\n      - name: Install Golang\n        uses: actions/setup-go@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          go-version: 1.17.1\n      - name: Gradle cache\n        uses: actions/cache@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          path: ~/.gradle\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Native Build\n        if: steps.cache.outputs.cache-hit != 'true'\n        run: ./run lib core\n  shadowsocks:\n    name: Native Build (shadowsocks-rust)\n    runs-on: ubuntu-latest\n    needs: check\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: git submodule status 'library/shadowsocks/*' > shadowsocks_status\n      - name: Shadowsocks Cache\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\n      - name: Install Rust\n        if: steps.cache.outputs.cache-hit != 'true'\n        run: ./run init action shadowsocks\n      - name: Gradle cache\n        uses: actions/cache@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          path: ~/.gradle\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Native Build\n        if: steps.cache.outputs.cache-hit != 'true'\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          ./run init action library\n          ./run lib shadowsocks\n  shadowsocks_libev:\n    name: Native Build (shadowsocks-libev)\n    runs-on: ubuntu-latest\n    needs: check\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\n      - name: Shadowsocks Cache\n        id: cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks-libev.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\n      - name: Gradle cache\n        uses: actions/cache@v2\n        if: steps.cache.outputs.cache-hit != 'true'\n        with:\n          path: ~/.gradle\n          key: native-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Native Build\n        if: steps.cache.outputs.cache-hit != 'true'\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          ./run init action library\n          ./run lib shadowsocks_libev\n  build:\n    name: Gradle Build\n    runs-on: ubuntu-latest\n    needs:\n      - libcore\n      - shadowsocks\n      - shadowsocks_libev\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: |\n          git submodule status 'library/shadowsocks/*' > shadowsocks_status\n          git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\n          git submodule status library/core > libcore_status\n      - name: LibCore Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/libcore.aar\n          key: ${{ hashFiles('.github/workflows/*', 'bin/lib/core/*', 'libcore_status') }}\n      - name: Shadowsocks Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\n      - name: Shadowsocks (libev) Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks-libev.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\n      - name: Gradle cache\n        uses: actions/cache@v2\n        with:\n          path: ~/.gradle\n          key: gradle-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Release Build\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          export LOCAL_PROPERTIES=\"${{ secrets.LOCAL_PROPERTIES }}\"\n          ./run init action library\n          ./gradlew app:assembleOssRelease\n          APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')\n          APK=$(dirname $APK)\n          echo \"APK=$APK\" >> $GITHUB_ENV\n      - uses: actions/upload-artifact@v2\n        with:\n          name: APKs\n          path: ${{ env.APK }}\n      - uses: actions/upload-artifact@v2\n        with:\n          name: \"SHA256-ARM ${{ env.SHA256_ARM }}\"\n          path: ${{ env.SUM_ARM }}\n      - uses: actions/upload-artifact@v2\n        with:\n          name: \"SHA256-ARM64 ${{ env.SHA256_ARM64 }}\"\n          path: ${{ env.SUM_ARM64 }}\n      - uses: actions/upload-artifact@v2\n        with:\n          name: \"SHA256-X64 ${{ env.SHA256_X64 }}\"\n          path: ${{ env.SUM_X64 }}\n      - uses: actions/upload-artifact@v2\n        with:\n          name: \"SHA256-X86 ${{ env.SHA256_X86 }}\"\n          path: ${{ env.SUM_X86 }}\n  publish:\n    name: Publish Release\n    if: github.event.inputs.publish != 'y'\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Donwload Artifacts\n        uses: actions/download-artifact@v2\n        with:\n          name: APKs\n          path: artifacts\n      - name: Release\n        run: |\n          wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz\n          tar -xvf ghr.tar.gz\n          mv ghr*linux_amd64/ghr .\n          mkdir apks\n          find artifacts -name \"*.apk\" -exec cp {} apks \\;\n          find artifacts -name \"*.sha256sum.txt\" -exec cp {} apks \\;\n          ./ghr -delete -t \"${{ github.token }}\" -n \"${{ github.event.inputs.tag }}\" \"${{ github.event.inputs.tag }}\" apks\n  upload:\n    name: Upload Release\n    if: github.event.inputs.upload != 'y'\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Donwload Artifacts\n        uses: actions/download-artifact@v2\n        with:\n          name: APKs\n          path: artifacts\n      - name: Release\n        run: |\n          mkdir apks\n          find artifacts -name \"*.apk\" -exec cp {} apks \\;\n\n          function upload() {\n            for apk in $@; do\n              echo \">> Uploading $apk\"\n              curl https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendDocument \\\n                -X POST \\\n                -F chat_id=\"${{ secrets.TELEGRAM_CHANNEL }}\" \\\n                -F document=\"@$apk\" \\\n                --silent --show-error --fail >/dev/null &\n            done\n            for job in $(jobs -p); do\n              wait $job || exit 1\n            done\n          }\n          upload apks/*\n  play:\n    name: Publish to Play Store\n    if: github.event.inputs.play != 'y'\n    runs-on: ubuntu-latest\n    needs:\n      - libcore\n      - shadowsocks\n      - shadowsocks_libev\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Fetch Status\n        run: |\n          git submodule status 'library/shadowsocks/*' > shadowsocks_status\n          git submodule status 'library/shadowsocks-libev/*' > shadowsocks_libev_status\n          git submodule status library/core > libcore_status\n      - name: LibCore Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/libcore.aar\n          key: ${{ hashFiles('.github/workflows/*', 'bin/lib/core/*', 'libcore_status') }}\n      - name: Shadowsocks Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks/build.gradle.kts', 'shadowsocks_status') }}\n      - name: Shadowsocks (libev) Cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            app/libs/shadowsocks-libev.aar\n          key: ${{ hashFiles('.github/workflows/*', 'library/shadowsocks-libev/build.gradle.kts', 'shadowsocks_libev_status') }}\n      - name: Gradle cache\n        uses: actions/cache@v2\n        with:\n          path: ~/.gradle\n          key: gradle-${{ hashFiles('**/*.gradle.kts') }}\n      - name: Checkout Library\n        run: |\n          git submodule update --init 'app/*'\n      - name: Release Build\n        env:\n          BUILD_PLUGIN: none\n        run: |\n          echo \"sdk.dir=${ANDROID_HOME}\" > local.properties\n          echo \"ndk.dir=${ANDROID_HOME}/ndk/21.4.7075529\" >> local.properties\n          export LOCAL_PROPERTIES=\"${{ secrets.LOCAL_PROPERTIES }}\"\n          cat > service_account_credentials.json << EOF\n          ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}\"\n          EOF\n          ./run init action library\n          ./gradlew app:publishPlayReleaseBundle"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\nbuild/\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n/app/libs/\n/app/src/main/assets/v2ray\n/service_account_credentials.json\njniLibs/"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"library/shadowsocks/src/main/rust/shadowsocks-rust\"]\n\tpath = library/shadowsocks/src/main/rust/shadowsocks-rust\n\turl = https://github.com/shadowsocks/shadowsocks-rust.git\n[submodule \"external/editorkit\"]\n\tpath = external/editorkit\n\turl = https://github.com/SagerNet/editorkit\n[submodule \"external/preferencex\"]\n\tpath = external/preferencex\n\turl = https://github.com/SagerNet/preferencex-android\n[submodule \"external/Xray-core\"]\n\tpath = external/Xray-core\n\turl = https://github.com/SagerNet/Xray-core\n[submodule \"library/core\"]\n\tpath = library/core\n\turl = https://github.com/XTLS/LibAnXrayCore\n[submodule \"external/termux-view\"]\n\tpath = external/termux-view\n\turl = https://github.com/SagerNet/termux-view\n[submodule \"library/shadowsocks-libev/src/main/jni/libev\"]\n\tpath = library/shadowsocks-libev/src/main/jni/libev\n\turl = https://git.lighttpd.net/mirrors/libev.git\n[submodule \"library/shadowsocks-libev/src/main/jni/libancillary\"]\n\tpath = library/shadowsocks-libev/src/main/jni/libancillary\n\turl = https://github.com/shadowsocks/libancillary.git\n[submodule \"library/shadowsocks-libev/src/main/jni/libevent\"]\n\tpath = library/shadowsocks-libev/src/main/jni/libevent\n\turl = https://github.com/shadowsocks/libevent.git\n[submodule \"library/shadowsocks-libev/src/main/jni/mbedtls\"]\n\tpath = library/shadowsocks-libev/src/main/jni/mbedtls\n\turl = https://github.com/SagerNet/mbedtls\n[submodule \"library/shadowsocks-libev/src/main/jni/pcre\"]\n\tpath = library/shadowsocks-libev/src/main/jni/pcre\n\turl = https://android.googlesource.com/platform/external/pcre\n[submodule \"library/shadowsocks-libev/src/main/jni/libsodium\"]\n\tpath = library/shadowsocks-libev/src/main/jni/libsodium\n\turl = https://github.com/jedisct1/libsodium.git\n\tbranch = stable\n[submodule \"library/shadowsocks-libev/src/main/jni/shadowsocks-libev\"]\n\tpath = library/shadowsocks-libev/src/main/jni/shadowsocks-libev\n\turl = https://github.com/SagerNet/shadowsocks-libev"
  },
  {
    "path": ".idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <JetCodeStyleSettings>\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </JetCodeStyleSettings>\n    <codeStyleSettings language=\"XML\">\n      <option name=\"FORCE_REARRANGE_MODE\" value=\"1\" />\n      <indentOptions>\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n      </indentOptions>\n      <arrangement>\n        <rules>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:android</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:id</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>style</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>ANDROID_ATTRIBUTE_ORDER</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>.*</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n        </rules>\n      </arrangement>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"kotlin\">\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n      <option name=\"KEEP_LINE_BREAKS\" value=\"false\" />\n      <option name=\"ALIGN_MULTILINE_PARAMETERS\" value=\"false\" />\n      <option name=\"EXTENDS_LIST_WRAP\" value=\"2\" />\n      <option name=\"METHOD_CALL_CHAIN_WRAP\" value=\"5\" />\n      <option name=\"ASSIGNMENT_WRAP\" value=\"0\" />\n      <option name=\"ENUM_CONSTANTS_WRAP\" value=\"2\" />\n    </codeStyleSettings>\n  </code_scheme>\n</component>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n  </state>\n</component>"
  },
  {
    "path": ".idea/compiler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <bytecodeTargetLevel target=\"11\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/copyright/profiles_settings.xml",
    "content": "<component name=\"CopyrightManager\">\n  <settings default=\"sagernet\">\n    <module2copyright>\n      <element module=\"Project Files\" copyright=\"sagernet\" />\n    </module2copyright>\n    <LanguageOptions name=\"JAVA\">\n      <option name=\"fileTypeOverride\" value=\"3\" />\n      <option name=\"separateBefore\" value=\"true\" />\n      <option name=\"separateAfter\" value=\"true\" />\n      <option name=\"box\" value=\"true\" />\n    </LanguageOptions>\n    <LanguageOptions name=\"Kotlin\">\n      <option name=\"fileTypeOverride\" value=\"3\" />\n      <option name=\"separateBefore\" value=\"true\" />\n      <option name=\"separateAfter\" value=\"true\" />\n      <option name=\"box\" value=\"true\" />\n    </LanguageOptions>\n  </settings>\n</component>"
  },
  {
    "path": ".idea/copyright/sagernet.xml",
    "content": "<component name=\"CopyrightManager\">\n  <copyright>\n    <option name=\"allowReplaceRegexp\" value=\"Copyright\" />\n    <option name=\"notice\" value=\"Copyright (C) 2021 by nekohasekai &lt;contact-git@sekai.icu&gt;&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10; (at your option) any later version.&#10; &#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.&#10;\" />\n    <option name=\"myName\" value=\"sagernet\" />\n  </copyright>\n</component>"
  },
  {
    "path": ".idea/dictionaries/sekai.xml",
    "content": "<component name=\"ProjectDictionaryState\">\n  <dictionary name=\"sekai\">\n    <words>\n      <w>acra</w>\n      <w>aead</w>\n      <w>alpn</w>\n      <w>blackhole</w>\n      <w>conns</w>\n      <w>conscrypt</w>\n      <w>dokodemo</w>\n      <w>downlink</w>\n      <w>fakedns</w>\n      <w>fdroid</w>\n      <w>geoip</w>\n      <w>geosite</w>\n      <w>grpc</w>\n      <w>gson</w>\n      <w>gvisor</w>\n      <w>libev</w>\n      <w>libnaive</w>\n      <w>libtrojan</w>\n      <w>loyalsoldier</w>\n      <w>naiveproxy</w>\n      <w>nativeproxy</w>\n      <w>naïve</w>\n      <w>nekohasekai</w>\n      <w>obfs</w>\n      <w>pingtunnel</w>\n      <w>proxychains</w>\n      <w>quic</w>\n      <w>relaybaton</w>\n      <w>rprx</w>\n      <w>sagernet</w>\n      <w>shadowsocks</w>\n      <w>shadowsocksr</w>\n      <w>snackbar</w>\n      <w>thiz</w>\n      <w>tproxy</w>\n      <w>transproxy</w>\n      <w>uplink</w>\n      <w>utls</w>\n      <w>vless</w>\n      <w>vmess</w>\n      <w>websocket</w>\n      <w>xray</w>\n      <w>xtls</w>\n    </words>\n  </dictionary>\n</component>"
  },
  {
    "path": ".idea/gradle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersion=\"1\" />\n  <component name=\"GradleSettings\">\n    <option name=\"linkedExternalProjectsSettings\">\n      <GradleProjectSettings>\n        <compositeConfiguration>\n          <compositeBuild compositeDefinitionSource=\"SCRIPT\">\n            <builds>\n              <build path=\"$PROJECT_DIR$/external/editorkit\" name=\"editorkit\">\n                <projects>\n                  <project path=\"$PROJECT_DIR$/external/editorkit\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/data\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/domain\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/editorkit\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/features\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/features/feature-editor\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/features/feature-ui\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/features/feature-utils\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/filesystems\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/filesystems/filesystem-base\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/filesystems/filesystem-local\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/languages\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/languages/language-base\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/languages/language-json\" />\n                  <project path=\"$PROJECT_DIR$/external/editorkit/languages/language-plaintext\" />\n                </projects>\n              </build>\n              <build path=\"$PROJECT_DIR$/external/termux-view\" name=\"termux-view\">\n                <projects>\n                  <project path=\"$PROJECT_DIR$/external/termux-view\" />\n                  <project path=\"$PROJECT_DIR$/external/termux-view/terminal-emulator\" />\n                  <project path=\"$PROJECT_DIR$/external/termux-view/terminal-view\" />\n                </projects>\n              </build>\n            </builds>\n          </compositeBuild>\n        </compositeConfiguration>\n        <option name=\"testRunner\" value=\"GRADLE\" />\n        <option name=\"disableWrapperSourceDistributionNotification\" value=\"true\" />\n        <option name=\"distributionType\" value=\"DEFAULT_WRAPPED\" />\n        <option name=\"externalProjectPath\" value=\"$PROJECT_DIR$\" />\n        <option name=\"gradleHome\" value=\"$USER_HOME$/.gradle/wrapper/dists/gradle-6.8.3-bin/7ykxq50lst7lb7wx1nijpicxn/gradle-6.8.3\" />\n        <option name=\"gradleJvm\" value=\"Android Studio default JDK\" />\n        <option name=\"modules\">\n          <set>\n            <option value=\"$PROJECT_DIR$\" />\n            <option value=\"$PROJECT_DIR$/app\" />\n            <option value=\"$PROJECT_DIR$/buildSrc\" />\n            <option value=\"$PROJECT_DIR$/external\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/data\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/domain\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/editorkit\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/features\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/features/feature-editor\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/features/feature-ui\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/features/feature-utils\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/filesystems\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/filesystems/filesystem-base\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/filesystems/filesystem-local\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/languages\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/languages/language-base\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/languages/language-json\" />\n            <option value=\"$PROJECT_DIR$/external/editorkit/languages/language-plaintext\" />\n            <option value=\"$PROJECT_DIR$/external/preferencex\" />\n            <option value=\"$PROJECT_DIR$/external/preferencex/colorpicker\" />\n            <option value=\"$PROJECT_DIR$/external/preferencex/flexbox\" />\n            <option value=\"$PROJECT_DIR$/external/preferencex/preferencex\" />\n            <option value=\"$PROJECT_DIR$/external/preferencex/preferencex-colorpicker\" />\n            <option value=\"$PROJECT_DIR$/external/preferencex/preferencex-simplemenu\" />\n            <option value=\"$PROJECT_DIR$/external/termux-view\" />\n            <option value=\"$PROJECT_DIR$/external/termux-view/terminal-emulator\" />\n            <option value=\"$PROJECT_DIR$/external/termux-view/terminal-view\" />\n            <option value=\"$PROJECT_DIR$/library\" />\n            <option value=\"$PROJECT_DIR$/library/include\" />\n            <option value=\"$PROJECT_DIR$/library/shadowsocks\" />\n            <option value=\"$PROJECT_DIR$/library/shadowsocks-libev\" />\n            <option value=\"$PROJECT_DIR$/library/stub\" />\n            <option value=\"$PROJECT_DIR$/plugin\" />\n            <option value=\"$PROJECT_DIR$/plugin/api\" />\n          </set>\n        </option>\n        <option name=\"resolveModulePerSourceSet\" value=\"false\" />\n      </GradleProjectSettings>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"BlockingMethodInNonBlockingContext\" enabled=\"false\" level=\"WARNING\" enabled_by_default=\"false\" />\n    <inspection_tool class=\"FunctionName\" enabled=\"false\" level=\"WEAK WARNING\" enabled_by_default=\"false\" />\n    <inspection_tool class=\"HasPlatformType\" enabled=\"false\" level=\"WEAK WARNING\" enabled_by_default=\"false\" />\n    <inspection_tool class=\"MemberVisibilityCanBePrivate\" enabled=\"false\" level=\"INFO\" enabled_by_default=\"false\" />\n    <inspection_tool class=\"ShellCheck\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <shellcheck_settings value=\"SC2086,SC2164\" />\n    </inspection_tool>\n    <inspection_tool class=\"UnusedSymbol\" enabled=\"false\" level=\"WARNING\" enabled_by_default=\"false\" />\n  </profile>\n</component>"
  },
  {
    "path": ".idea/jarRepositories.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RemoteRepositoriesConfiguration\">\n    <remote-repository>\n      <option name=\"id\" value=\"central\" />\n      <option name=\"name\" value=\"Maven Central repository\" />\n      <option name=\"url\" value=\"https://repo1.maven.org/maven2\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"jboss.community\" />\n      <option name=\"name\" value=\"JBoss Community repository\" />\n      <option name=\"url\" value=\"https://repository.jboss.org/nexus/content/repositories/public/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"BintrayJCenter\" />\n      <option name=\"name\" value=\"BintrayJCenter\" />\n      <option name=\"url\" value=\"https://jcenter.bintray.com/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"Google\" />\n      <option name=\"name\" value=\"Google\" />\n      <option name=\"url\" value=\"https://dl.google.com/dl/android/maven2/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"maven\" />\n      <option name=\"name\" value=\"maven\" />\n      <option name=\"url\" value=\"https://jitpack.io\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"MavenRepo\" />\n      <option name=\"name\" value=\"MavenRepo\" />\n      <option name=\"url\" value=\"https://repo.maven.apache.org/maven2/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"maven\" />\n      <option name=\"name\" value=\"maven\" />\n      <option name=\"url\" value=\"https://dl.bintray.com/takisoft/android\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"Gradle Central Plugin Repository2\" />\n      <option name=\"name\" value=\"Gradle Central Plugin Repository2\" />\n      <option name=\"url\" value=\"https://plugins.gradle.org/m2\" />\n    </remote-repository>\n  </component>\n</project>"
  },
  {
    "path": ".idea/kotlinScripting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinScriptingSettings\">\n    <option name=\"suppressDefinitionsCheck\" value=\"true\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"DesignSurface\">\n    <option name=\"filePathToZoomLevelMap\">\n      <map>\n        <entry key=\"../../../../layout/custom_preview.xml\" value=\"0.39375\" />\n        <entry key=\"../../.gradle/caches/transforms-3/60c79b6b6cf50df0de760b958b8bc3be/transformed/material-1.3.0/res/drawable/design_snackbar_background.xml\" value=\"0.147\" />\n        <entry key=\"../../.local/lib/android/sdk/platforms/android-30/data/res/layout-watch/preference_material.xml\" value=\"0.17435897435897435\" />\n        <entry key=\"app/src/main/res/drawable/baseline_wrap_text_24.xml\" value=\"0.2085\" />\n        <entry key=\"app/src/main/res/drawable/ic_action_copyright.xml\" value=\"0.3375\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_cast_connected_24.xml\" value=\"0.22239583333333332\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml\" value=\"0.3375\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml\" value=\"0.207\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_http_24.xml\" value=\"0.3375\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_info_24.xml\" value=\"0.218\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_nat_24.xml\" value=\"0.182\" />\n        <entry key=\"app/src/main/res/drawable/ic_baseline_system_update_alt_24.xml\" value=\"0.1\" />\n        <entry key=\"app/src/main/res/drawable/ic_file_file_upload.xml\" value=\"0.268\" />\n        <entry key=\"app/src/main/res/drawable/ic_image_edit.xml\" value=\"0.2265\" />\n        <entry key=\"app/src/main/res/drawable/ic_navigation_close.xml\" value=\"0.285\" />\n        <entry key=\"app/src/main/res/drawable/ic_service_active.xml\" value=\"0.135\" />\n        <entry key=\"app/src/main/res/drawable/ic_service_idle.xml\" value=\"0.2255\" />\n        <entry key=\"app/src/main/res/drawable/ic_service_stopped.xml\" value=\"0.2035\" />\n        <entry key=\"app/src/main/res/drawable/ic_settings_password.xml\" value=\"0.3375\" />\n        <entry key=\"app/src/main/res/drawable/ic_social_share.xml\" value=\"0.283\" />\n        <entry key=\"app/src/main/res/drawable/navigation_icon.xml\" value=\"0.1715\" />\n        <entry key=\"app/src/main/res/drawable/terminal_scroll_shape.xml\" value=\"0.1935\" />\n        <entry key=\"app/src/main/res/layout/layout_about.xml\" value=\"0.33\" />\n        <entry key=\"app/src/main/res/layout/layout_active_item.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_active_list.xml\" value=\"0.13333333333333333\" />\n        <entry key=\"app/src/main/res/layout/layout_app_list.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_appbar.xml\" value=\"0.2490740740740741\" />\n        <entry key=\"app/src/main/res/layout/layout_apps.xml\" value=\"0.2490740740740741\" />\n        <entry key=\"app/src/main/res/layout/layout_apps_item.xml\" value=\"0.2490740740740741\" />\n        <entry key=\"app/src/main/res/layout/layout_asset_item.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_assets.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_chain_settings.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_cloudflare.xml\" value=\"0.2909441233140655\" />\n        <entry key=\"app/src/main/res/layout/layout_config_settings.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_debug.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_edit_config.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_edit_group.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_empty.xml\" value=\"0.26033755274261605\" />\n        <entry key=\"app/src/main/res/layout/layout_group.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_group_item.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_group_list.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_license.xml\" value=\"0.26033755274261605\" />\n        <entry key=\"app/src/main/res/layout/layout_link_dialog.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_loading.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_logcat.xml\" value=\"0.10462962962962963\" />\n        <entry key=\"app/src/main/res/layout/layout_main.xml\" value=\"0.22\" />\n        <entry key=\"app/src/main/res/layout/layout_navigation.xml\" value=\"0.19786324786324785\" />\n        <entry key=\"app/src/main/res/layout/layout_navigation_black.xml\" value=\"0.19786324786324785\" />\n        <entry key=\"app/src/main/res/layout/layout_password_dialog.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_process.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_profile.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/layout/layout_profile_clash.xml\" value=\"0.2490740740740741\" />\n        <entry key=\"app/src/main/res/layout/layout_profile_list.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_progess.xml\" value=\"0.15864022662889518\" />\n        <entry key=\"app/src/main/res/layout/layout_progress.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_route.xml\" value=\"0.13333333333333333\" />\n        <entry key=\"app/src/main/res/layout/layout_route_item.xml\" value=\"0.5\" />\n        <entry key=\"app/src/main/res/layout/layout_scanner.xml\" value=\"0.2490740740740741\" />\n        <entry key=\"app/src/main/res/layout/layout_settings_activity.xml\" value=\"0.17844202898550723\" />\n        <entry key=\"app/src/main/res/layout/layout_tools.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/layout/layout_traffic.xml\" value=\"0.13333333333333333\" />\n        <entry key=\"app/src/main/res/layout/layout_traffic_item.xml\" value=\"0.22192028985507245\" />\n        <entry key=\"app/src/main/res/layout/layout_traffic_list.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/add_group_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/add_profile_menu.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/menu/add_route_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/app_list_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/export_only_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/group_action_menu.xml\" value=\"0.21581196581196582\" />\n        <entry key=\"app/src/main/res/menu/import_asset_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/logcat_menu.xml\" value=\"0.26324786324786326\" />\n        <entry key=\"app/src/main/res/menu/main_drawer_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/per_app_proxy_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/profile_apply_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/profile_config_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/profile_share_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/scanner_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/socks_share_menu.xml\" value=\"0.1\" />\n        <entry key=\"app/src/main/res/menu/traffic_item_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/traffic_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/menu/vmess_share_menu.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml\" value=\"0.186\" />\n        <entry key=\"app/src/main/res/xml/balancer_preferences.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/xml/brook_preferences.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/xml/chain_preferences.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/xml/config_preferences.xml\" value=\"0.33\" />\n        <entry key=\"app/src/main/res/xml/global_preferences.xml\" value=\"1.0\" />\n        <entry key=\"app/src/main/res/xml/group_preferences.xml\" value=\"0.16\" />\n        <entry key=\"app/src/main/res/xml/http_preferences.xml\" value=\"0.32135416666666666\" />\n        <entry key=\"app/src/main/res/xml/hysteria_preferences.xml\" value=\"0.1\" />\n        <entry key=\"app/src/main/res/xml/name_preferences.xml\" value=\"0.1423076923076923\" />\n        <entry key=\"app/src/main/res/xml/pingtunnel_preferences.xml\" value=\"0.19907407407407407\" />\n        <entry key=\"app/src/main/res/xml/relaybaton_preferences.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/xml/route_preferences.xml\" value=\"0.33\" />\n        <entry key=\"app/src/main/res/xml/socks_preferences.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"app/src/main/res/xml/ssh_preferences.xml\" value=\"0.14565826330532214\" />\n        <entry key=\"app/src/main/res/xml/standard_v2ray_preferences.xml\" value=\"0.33\" />\n        <entry key=\"app/src/main/res/xml/trojan_go_preferences.xml\" value=\"0.14316239316239315\" />\n        <entry key=\"app/src/main/res/xml/trojan_preferences.xml\" value=\"0.192481884057971\" />\n        <entry key=\"external/editorkit/app/src/main/res/layout/activity_main.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"external/editorkit/features/feature-editor/src/main/res/drawable/autocomplete_bg.xml\" value=\"0.2315\" />\n        <entry key=\"external/editorkit/features/feature-editor/src/main/res/layout/dialog_save_as.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"external/editorkit/features/feature-editor/src/main/res/layout/fragment_editor.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"external/editorkit/features/feature-editor/src/main/res/layout/item_keyboard_key.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"external/preferencex-simplemenu/src/main/res/drawable/simple_menu_background.xml\" value=\"0.147\" />\n        <entry key=\"external/preferencex/preferencex-simplemenu/src/main/res/layout/simple_menu_list.xml\" value=\"0.2636752136752137\" />\n        <entry key=\"naive-plugin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml\" value=\"0.20925925925925926\" />\n      </map>\n    </option>\n  </component>\n  <component name=\"ExternalStorageConfigurationManager\" enabled=\"true\" />\n  <component name=\"ProjectRootManager\" version=\"2\" languageLevel=\"JDK_11\" default=\"true\" project-jdk-name=\"11\" project-jdk-type=\"JavaSDK\">\n    <output url=\"file://$PROJECT_DIR$/build/classes\" />\n  </component>\n  <component name=\"ProjectType\">\n    <option name=\"id\" value=\"Android\" />\n  </component>\n  <component name=\"SuppressKotlinCodeStyleNotification\">\n    <option name=\"disableForAll\" value=\"true\" />\n  </component>\n  <component name=\"VisualizationToolProject\">\n    <option name=\"state\">\n      <ProjectState>\n        <option name=\"scale\" value=\"0.22\" />\n      </ProjectState>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GitSharedSettings\">\n    <option name=\"FORCE_PUSH_PROHIBITED_PATTERNS\">\n      <list />\n    </option>\n  </component>\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/external/Xray-core\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/external/editorkit\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/external/preferencex\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/external/termux-view\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/core\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/libancillary\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/libev\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/libevent\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/libsodium\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/mbedtls\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/pcre\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/shadowsocks-libev\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/shadowsocks-libev/libbloom\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/shadowsocks-libev/libcork\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks-libev/src/main/jni/shadowsocks-libev/libipset\" vcs=\"Git\" />\n    <mapping directory=\"$PROJECT_DIR$/library/shadowsocks/src/main/rust/shadowsocks-rust\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "AUTHORS",
    "content": "SagerNet was originally created in late 2021, by\nnekohasekai <contact-sagernet@sekai.icu>.\n\nHere is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --\npeople who have submitted patches, fixed bugs, added translations, and\ngenerally made SagerNet that much better:\n\nhttps://github.com/SagerNet/SagerNet/graphs/contributors\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see <http://www.gnu.org/licenses/>."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# ![AnXray](https://github.com/XTLS/AnXray/raw/img/screenshots/0.png)\n\nAnother Xray for Android.\n\n[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)\n[![Releases](https://img.shields.io/github/downloads/XTLS/AnXray/total.svg)](https://github.com/XTLS/AnXray/releases)\n[![Language: Kotlin](https://img.shields.io/github/languages/top/XTLS/AnXray.svg)](https://github.com/XTLS/AnXray/search?l=kotlin)\n[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-orange.svg)](https://www.gnu.org/licenses/gpl-3.0)\n\n</div>\n\n## SCREENSHOTS\n\nThe X-style logo, slogan, and exclusive bright & dark themes designed by [RPRX](https://github.com/rprx), the Chief Visual Designer at AnXray.\n\n<img src=\"https://github.com/XTLS/AnXray/raw/img/screenshots/1.jpg\" width=\"270\"> <img src=\"https://github.com/XTLS/AnXray/raw/img/screenshots/2.jpg\" width=\"270\"> <img src=\"https://github.com/XTLS/AnXray/raw/img/screenshots/3.jpg\" width=\"270\">\n\n<img src=\"https://github.com/XTLS/AnXray/raw/img/screenshots/4.jpg\" width=\"270\"> <img src=\"https://github.com/XTLS/AnXray/raw/img/screenshots/5.jpg\" width=\"270\"> <img src=\"https://github.com/XTLS/AnXray/raw/img/screenshots/6.jpg\" width=\"270\">\n\n## Documents\n\nhttps://anxray.org\n\n### Protocols\n\nThe application is designed to be used whenever possible.\n\n#### Proxy\n\n* SOCKS (4/4a/5)\n* HTTP(S)\n* SSH\n* Shadowsocks\n* ShadowsocksR\n* VMess\n* VLESS with XTLS support\n* Trojan with XTLS support\n* Snell\n* Trojan-Go ( trojan-go-plugin )\n* NaïveProxy ( naive-plugin )\n* relaybaton ( relaybaton-plugin )\n* Brook ( brook-plugin )\n* Hysteria ( hysteria-plugin )\n* WireGuard ( wireguard-plugin )\n\n##### ROOT required\n\n* Ping Tunnel ( pingtunnel-plugin )\n\n#### Subscription\n\n* Raw: All widely used formats (base64, clash or origin configuration)\n* [Open Online Config](https://github.com/Shadowsocks-NET/OpenOnlineConfig)\n* [Shadowsocks SIP008](https://shadowsocks.org/en/wiki/SIP008-Online-Configuration-Delivery.html)\n\n#### Features\n\n* Full basic features\n* Xray WebSocket browser dialer\n* Option to change the notification update interval\n* A Chinese apps scanner (based on dex classpath scanning, so it may be slower)\n* Proxy chain\n* Balancer\n* Advanced routing with outbound profile selection\n* Reverse proxy\n* Custom config (Xray / Trojan-Go)\n* Traffic statistics support, including real-time display and cumulative statistics\n* Foreground status based routing support\n\n## Credits\n\n<ul>\n    <li><a href=\"https://github.com/shadowsocks/shadowsocks-android\">shadowsocks/shadowsocks-android</a>: <code>GPL 3.0</code></li>\n    <li><a href=\"https://github.com/shadowsocksRb/shadowsocksr-libev/blob/master/LICENSE\">shadowsocksRb/shadowsocksr-libev</a>: <code>GPL 3.0</code></li>\n    <li><a href=\"https://github.com/p4gefau1t/trojan-go/blob/master/LICENSE\">p4gefau1t/Trojan-Go</a>: <code>GPL 3.0</code></li>\n    <li><a href=\"https://github.com/klzgrad/naiveproxy/blob/master/LICENSE\">klzgrad/naiveproxy</a>:  <code>BSD-3-Clause License</code></li>\n    <li><a href=\"https://github.com/esrrhs/pingtunnel/blob/master/LICENSE\">esrrhs/pingtunnel</a>:  <code>MIT</code></li>\n    <li><a href=\"https://github.com/iyouport-org/relaybaton/blob/ech/LICENSE\">iyouport-org/relaybaton</a>:  <code>MIT</code></li>\n    <li><a href=\"https://github.com/txthinking/brook/blob/master/LICENSE\">txthinking/brook</a>:  <code>GPL 3.0</code></li>\n    <li><a href=\"https://github.com/HyNetwork/hysteria/blob/master/LICENSE.md\">HyNetwork/hysteria</a>:  <code>MIT</code></li>\n    <li><a href=\"https://github.com/WireGuard/wireguard-go/blob/master/LICENSE\">WireGuard/wireguard-go</a>:  <code>MIT</code></li>\n\n</ul>\n\n## License\n\n```\nCopyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see <http://www.gnu.org/licenses/>.\n```"
  },
  {
    "path": "app/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.application\")\n    id(\"kotlin-android\")\n    id(\"kotlin-kapt\")\n    id(\"kotlin-parcelize\")\n    id(\"com.mikepenz.aboutlibraries.plugin\")\n    id(\"com.google.protobuf\")\n}\n\nsetupApp()\n\nandroid {\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n    }\n    kapt.arguments {\n        arg(\"room.incremental\", true)\n        arg(\"room.schemaLocation\", \"$projectDir/schemas\")\n    }\n    bundle {\n        language {\n            enableSplit = false\n        }\n    }\n    buildFeatures {\n        viewBinding = true\n    }\n}\n\ndependencies {\n\n    implementation(fileTree(\"libs\"))\n    compileOnly(project(\":library:stub\"))\n    implementation(project(\":library:include\"))\n\n    implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2\")\n    implementation(\"androidx.core:core-ktx:1.6.0\")\n    implementation(\"androidx.activity:activity-ktx:1.3.1\")\n    implementation(\"androidx.fragment:fragment-ktx:1.3.6\")\n    implementation(\"androidx.browser:browser:1.3.0\")\n\n    implementation(\"androidx.constraintlayout:constraintlayout:2.1.1\")\n    implementation(\"androidx.navigation:navigation-fragment-ktx:2.3.5\")\n    implementation(\"androidx.navigation:navigation-ui-ktx:2.3.5\")\n    implementation(\"androidx.preference:preference-ktx:1.1.1\")\n    implementation(\"androidx.appcompat:appcompat:1.3.1\")\n    implementation(\"androidx.work:work-runtime-ktx:2.7.0\")\n    implementation(\"androidx.work:work-multiprocess:2.7.0\")\n\n    implementation(project(\":external:preferencex:preferencex\"))\n    implementation(project(\":external:preferencex:preferencex-simplemenu\"))\n    implementation(project(\":external:preferencex:preferencex-colorpicker\"))\n\n    implementation(\"com.google.android.material:material:1.4.0\")\n    implementation(\"cn.hutool:hutool-core:5.7.14\")\n    implementation(\"cn.hutool:hutool-cache:5.7.14\")\n    implementation(\"cn.hutool:hutool-json:5.7.14\")\n    implementation(\"cn.hutool:hutool-crypto:5.7.14\")\n    implementation(\"com.google.code.gson:gson:2.8.8\")\n    implementation(\"com.google.zxing:core:3.4.1\")\n\n    implementation(platform(\"com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.2\"))\n    implementation(\"com.squareup.okhttp3:okhttp\")\n    implementation(\"com.squareup.okhttp3:okhttp-dnsoverhttps\")\n\n    implementation(\"org.yaml:snakeyaml:1.29\")\n    implementation(\"com.github.daniel-stoneuk:material-about-library:3.2.0-rc01\")\n    implementation(\"com.mikepenz:aboutlibraries:8.9.3\")\n    implementation(\"com.jakewharton:process-phoenix:2.1.2\")\n    implementation(\"com.esotericsoftware:kryo:5.2.0\")\n    implementation(\"org.conscrypt:conscrypt-android:2.5.2\")\n    implementation(\"com.google.guava:guava:31.0.1-android\")\n    implementation(\"com.journeyapps:zxing-android-embedded:4.2.0\")\n    implementation(\"org.ini4j:ini4j:0.5.4\")\n\n    implementation(\"com.simplecityapps:recyclerview-fastscroll:2.0.1\") {\n        exclude(group = \"androidx.recyclerview\")\n        exclude(group = \"androidx.appcompat\")\n    }\n    implementation(\"org.smali:dexlib2:2.5.2\") {\n        exclude(group = \"com.google.guava\", module = \"guava\")\n    }\n\n    implementation(\"androidx.room:room-runtime:2.3.0\")\n    kapt(\"androidx.room:room-compiler:2.3.0\")\n    implementation(\"androidx.room:room-ktx:2.3.0\")\n    implementation(\"com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4\")\n    kapt(\"com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4\")\n\n\n    implementation(\"editorkit:editorkit:2.0.0\")\n    implementation(\"editorkit:feature-editor:2.0.0\")\n    implementation(\"editorkit:language-json:2.0.0\")\n    implementation(\"termux:terminal-view:1.0\")\n\n\n//    implementation(project(\":library:proto-stub\"))\n//    implementation(\"io.grpc:grpc-okhttp:1.40.1\")\n\n    coreLibraryDesugaring(\"com.android.tools:desugar_jdk_libs:1.1.5\")\n}\n\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "-repackageclasses ''\n-allowaccessmodification\n\n-keep class io.nekohasekai.sagernet.** { *;}\n# ini4j\n-keep public class org.ini4j.spi.** { <init>(); }\n\n# SnakeYaml\n-keep class org.yaml.snakeyaml.** { *; }\n\n# IDK Why\n-keep class cn.hutool.core.convert.** { *; }\n\n-dontobfuscate\n-keepattributes SourceFile\n\n-dontwarn java.beans.BeanInfo\n-dontwarn java.beans.FeatureDescriptor\n-dontwarn java.beans.IntrospectionException\n-dontwarn java.beans.Introspector\n-dontwarn java.beans.PropertyDescriptor\n-dontwarn java.beans.Transient\n-dontwarn java.beans.VetoableChangeListener\n-dontwarn java.beans.VetoableChangeSupport\n-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl\n-dontwarn org.bouncycastle.jce.provider.BouncyCastleProvider\n-dontwarn org.bouncycastle.jsse.BCSSLParameters\n-dontwarn org.bouncycastle.jsse.BCSSLSocket\n-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider\n-dontwarn org.openjsse.javax.net.ssl.SSLParameters\n-dontwarn org.openjsse.javax.net.ssl.SSLSocket\n-dontwarn org.openjsse.net.ssl.OpenJSSE\n-dontwarn com.android.org.conscrypt.SSLParametersImpl"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"1239f165d8e5bf435ab3644e8fe25ff2\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1239f165d8e5bf435ab3644e8fe25ff2')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/10.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 10,\n    \"identityHash\": \"e429a05e6fa8d85cb18332d6a20b490e\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"sshBean\",\n            \"columnName\": \"sshBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"wgBean\",\n            \"columnName\": \"wgBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL, `appStatus` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appStatus\",\n            \"columnName\": \"appStatus\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"stats\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tcpConnections\",\n            \"columnName\": \"tcpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"udpConnections\",\n            \"columnName\": \"udpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uplink\",\n            \"columnName\": \"uplink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downlink\",\n            \"columnName\": \"downlink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_stats_packageName\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"packageName\"\n            ],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_stats_packageName` ON `${TABLE_NAME}` (`packageName`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e429a05e6fa8d85cb18332d6a20b490e')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"7d7e2a82a10090ef33d4144c968a0261\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7d7e2a82a10090ef33d4144c968a0261')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"3e2a0dd9879b5afd0ca3e9d55c7e0347\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e2a0dd9879b5afd0ca3e9d55c7e0347')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/4.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 4,\n    \"identityHash\": \"695d898dfaf50d8e53a65332eeae0f8f\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '695d898dfaf50d8e53a65332eeae0f8f')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/5.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 5,\n    \"identityHash\": \"255dcce3959e7a074a5c7835554e061d\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '255dcce3959e7a074a5c7835554e061d')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 6,\n    \"identityHash\": \"defc60daae7ba00e68f5aade29470de7\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"stats\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL, PRIMARY KEY(`packageName`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tcpConnections\",\n            \"columnName\": \"tcpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"udpConnections\",\n            \"columnName\": \"udpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uplink\",\n            \"columnName\": \"uplink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downlink\",\n            \"columnName\": \"downlink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"packageName\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'defc60daae7ba00e68f5aade29470de7')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/7.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 7,\n    \"identityHash\": \"e957a0581037cce1c1f33c637f856cea\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"stats\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tcpConnections\",\n            \"columnName\": \"tcpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"udpConnections\",\n            \"columnName\": \"udpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uplink\",\n            \"columnName\": \"uplink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downlink\",\n            \"columnName\": \"downlink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_stats_packageName\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"packageName\"\n            ],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_stats_packageName` ON `${TABLE_NAME}` (`packageName`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e957a0581037cce1c1f33c637f856cea')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/8.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 8,\n    \"identityHash\": \"f494636dc7d13a464b5d27634324ae0a\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL, `appStatus` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appStatus\",\n            \"columnName\": \"appStatus\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"stats\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tcpConnections\",\n            \"columnName\": \"tcpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"udpConnections\",\n            \"columnName\": \"udpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uplink\",\n            \"columnName\": \"uplink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downlink\",\n            \"columnName\": \"downlink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_stats_packageName\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"packageName\"\n            ],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_stats_packageName` ON `${TABLE_NAME}` (`packageName`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f494636dc7d13a464b5d27634324ae0a')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 9,\n    \"identityHash\": \"aa700a039e9ba16631c1a023f4355410\",\n    \"entities\": [\n      {\n        \"tableName\": \"proxy_groups\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ungrouped\",\n            \"columnName\": \"ungrouped\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subscription\",\n            \"columnName\": \"subscription\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"proxy_entities\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `ptBean` BLOB, `rbBean` BLOB, `brookBean` BLOB, `hysteriaBean` BLOB, `snellBean` BLOB, `sshBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupId\",\n            \"columnName\": \"groupId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tx\",\n            \"columnName\": \"tx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"rx\",\n            \"columnName\": \"rx\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"status\",\n            \"columnName\": \"status\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ping\",\n            \"columnName\": \"ping\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uuid\",\n            \"columnName\": \"uuid\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"error\",\n            \"columnName\": \"error\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"socksBean\",\n            \"columnName\": \"socksBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"httpBean\",\n            \"columnName\": \"httpBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssBean\",\n            \"columnName\": \"ssBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ssrBean\",\n            \"columnName\": \"ssrBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vmessBean\",\n            \"columnName\": \"vmessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"vlessBean\",\n            \"columnName\": \"vlessBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanBean\",\n            \"columnName\": \"trojanBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"trojanGoBean\",\n            \"columnName\": \"trojanGoBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"naiveBean\",\n            \"columnName\": \"naiveBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"ptBean\",\n            \"columnName\": \"ptBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"rbBean\",\n            \"columnName\": \"rbBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"brookBean\",\n            \"columnName\": \"brookBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"hysteriaBean\",\n            \"columnName\": \"hysteriaBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"snellBean\",\n            \"columnName\": \"snellBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"sshBean\",\n            \"columnName\": \"sshBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"configBean\",\n            \"columnName\": \"configBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"chainBean\",\n            \"columnName\": \"chainBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"balancerBean\",\n            \"columnName\": \"balancerBean\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"groupId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"groupId\"\n            ],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"rules\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `reverse` INTEGER NOT NULL, `redirect` TEXT NOT NULL, `packages` TEXT NOT NULL, `appStatus` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"userOrder\",\n            \"columnName\": \"userOrder\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enabled\",\n            \"columnName\": \"enabled\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domains\",\n            \"columnName\": \"domains\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ip\",\n            \"columnName\": \"ip\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"port\",\n            \"columnName\": \"port\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sourcePort\",\n            \"columnName\": \"sourcePort\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"network\",\n            \"columnName\": \"network\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"source\",\n            \"columnName\": \"source\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"protocol\",\n            \"columnName\": \"protocol\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"attrs\",\n            \"columnName\": \"attrs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"outbound\",\n            \"columnName\": \"outbound\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"reverse\",\n            \"columnName\": \"reverse\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"redirect\",\n            \"columnName\": \"redirect\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packages\",\n            \"columnName\": \"packages\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appStatus\",\n            \"columnName\": \"appStatus\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"stats\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"packageName\",\n            \"columnName\": \"packageName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"tcpConnections\",\n            \"columnName\": \"tcpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"udpConnections\",\n            \"columnName\": \"udpConnections\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"uplink\",\n            \"columnName\": \"uplink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downlink\",\n            \"columnName\": \"downlink\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_stats_packageName\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"packageName\"\n            ],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_stats_packageName` ON `${TABLE_NAME}` (`packageName`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aa700a039e9ba16631c1a023f4355410')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/io.nekohasekai.sagernet.database.preference.PublicDatabase/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"f1aab1fb633378621635c344dbc8ac7b\",\n    \"entities\": [\n      {\n        \"tableName\": \"KeyValuePair\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"key\",\n            \"columnName\": \"key\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"valueType\",\n            \"columnName\": \"valueType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"value\",\n            \"affinity\": \"BLOB\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"key\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f1aab1fb633378621635c344dbc8ac7b')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/src/androidTest/java/io/nekohasekai/sagernet/ExampleInstrumentedTest.kt",
    "content": "package io.nekohasekai.sagernet\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"io.nekohasekai.sagernet\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "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    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"io.nekohasekai.sagernet\"\n    android:installLocation=\"internalOnly\">\n\n    <uses-sdk tools:overrideLibrary=\"com.google.zxing.client.android\" />\n\n    <permission\n        android:name=\"${applicationId}.SERVICE\"\n        android:protectionLevel=\"signature\" />\n\n    <uses-permission android:name=\"${applicationId}.SERVICE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n\n    <uses-permission\n        android:name=\"android.permission.QUERY_ALL_PACKAGES\"\n        tools:ignore=\"QueryAllPackagesPermission\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS\" />\n\n    <uses-feature\n        android:name=\"android.software.leanback\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.touchscreen\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.camera\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.camera.autofocus\"\n        android:required=\"false\" />\n\n    <queries>\n        <intent>\n            <action android:name=\"com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN\" />\n        </intent>\n        <intent>\n            <action android:name=\"io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN\" />\n        </intent>\n    </queries>\n\n    <application\n        android:name=\"io.nekohasekai.sagernet.SagerNet\"\n        android:allowBackup=\"true\"\n        android:autoRevokePermissions=\"allowed\"\n        android:banner=\"@mipmap/ic_launcher\"\n        android:extractNativeLibs=\"true\"\n        android:fullBackupContent=\"@xml/backup_descriptor\"\n        android:fullBackupOnly=\"true\"\n        android:hardwareAccelerated=\"true\"\n        android:hasFragileUserData=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:largeHeap=\"true\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.SagerNet.Start\">\n        <meta-data\n            android:name=\"android.app.shortcuts\"\n            android:resource=\"@xml/shortcuts\" />\n\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.MainActivity\"\n            android:configChanges=\"uiMode\"\n            android:exported=\"true\"\n            android:label=\"@string/app_name\"\n            android:launchMode=\"singleTask\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE_PREFERENCES\" />\n            </intent-filter>\n\n            <intent-filter android:label=\"@string/subscription_import\">\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:host=\"subscription\"\n                    android:scheme=\"sn\" />\n\n            </intent-filter>\n\n            <intent-filter android:label=\"@string/subscription_import\">\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:host=\"install-config\"\n                    android:scheme=\"clash\" />\n            </intent-filter>\n\n            <intent-filter android:label=\"@string/profile_import\">\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 android:scheme=\"sn\" />\n                <data android:scheme=\"ss\" />\n                <data android:scheme=\"ssr\" />\n                <data android:scheme=\"socks\" />\n                <data android:scheme=\"socks4\" />\n                <data android:scheme=\"socksa\" />\n                <data android:scheme=\"sock5\" />\n                <data android:scheme=\"vmess\" />\n                <data android:scheme=\"vless\" />\n                <data android:scheme=\"trojan\" />\n                <data android:scheme=\"trojan-go\" />\n                <data android:scheme=\"naive+https\" />\n                <data android:scheme=\"naive+quic\" />\n                <data android:scheme=\"pingtunnel\" />\n                <data android:scheme=\"relaybaton\" />\n                <data android:scheme=\"brook\" />\n\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.shortcuts\"\n                android:resource=\"@xml/shortcuts\" />\n        </activity>\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.VpnRequestActivity\"\n            android:excludeFromRecents=\"true\"\n            android:taskAffinity=\"\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.HttpSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.ShadowsocksRSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.VLESSSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.TrojanSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.TrojanGoSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.NaiveSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.PingTunnelSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.RelayBatonSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.BrookSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.HysteriaSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.SnellSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.SSHSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.ConfigSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.ConfigEditActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.ChainSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <!--<activity\n            android:name=\"io.nekohasekai.sagernet.ui.profile.BalancerSettingsActivity\"\n            android:configChanges=\"uiMode\" />-->\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.GroupSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.RouteSettingsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.AssetsActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.AppListActivity\"\n            android:configChanges=\"uiMode\" />\n        <activity\n            android:name=\".QuickToggleShortcut\"\n            android:excludeFromRecents=\"true\"\n            android:exported=\"true\"\n            android:label=\"@string/quick_toggle\"\n            android:launchMode=\"singleTask\"\n            android:process=\":bg\"\n            android:taskAffinity=\"\"\n            android:theme=\"@android:style/Theme.Translucent.NoTitleBar\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.CREATE_SHORTCUT\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.AppManagerActivity\"\n            android:configChanges=\"uiMode\"\n            android:excludeFromRecents=\"true\"\n            android:label=\"@string/proxied_apps\"\n            android:launchMode=\"singleTask\"\n            android:parentActivityName=\".ui.MainActivity\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.ScannerActivity\"\n            android:configChanges=\"uiMode\"\n            android:excludeFromRecents=\"true\"\n            android:label=\"@string/add_profile_methods_scan_qr_code\"\n            android:launchMode=\"singleTask\"\n            android:parentActivityName=\"io.nekohasekai.sagernet.ui.MainActivity\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.LicenseActivity\"\n            android:configChanges=\"uiMode\"\n            android:label=\"@string/oss_licenses\"\n            android:launchMode=\"singleTask\"\n            android:parentActivityName=\"io.nekohasekai.sagernet.ui.MainActivity\" />\n        <activity\n            android:name=\"io.nekohasekai.sagernet.ui.ProfileSelectActivity\"\n            android:configChanges=\"uiMode\"\n            android:label=\"@string/select_profile\"\n            android:launchMode=\"singleTask\"\n            android:parentActivityName=\"io.nekohasekai.sagernet.ui.MainActivity\" />\n\n        <service\n            android:name=\"io.nekohasekai.sagernet.bg.ProxyService\"\n            android:directBootAware=\"true\"\n            android:exported=\"false\"\n            android:process=\":bg\" />\n\n        <service\n            android:name=\"io.nekohasekai.sagernet.bg.VpnService\"\n            android:directBootAware=\"true\"\n            android:exported=\"false\"\n            android:label=\"@string/app_name\"\n            android:permission=\"android.permission.BIND_VPN_SERVICE\"\n            android:process=\":bg\">\n\n            <intent-filter>\n                <action android:name=\"android.net.VpnService\" />\n            </intent-filter>\n        </service>\n\n        <service\n            android:name=\"io.nekohasekai.sagernet.bg.TileService\"\n            android:directBootAware=\"true\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_service_ax\"\n            android:label=\"@string/tile_title\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\"\n            android:process=\":bg\"\n            tools:targetApi=\"n\">\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n        </service>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.log\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/log_paths\" />\n        </provider>\n\n        <receiver\n            android:name=\"io.nekohasekai.sagernet.BootReceiver\"\n            android:directBootAware=\"true\"\n            android:enabled=\"false\"\n            android:exported=\"true\"\n            android:process=\":bg\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n                <action android:name=\"android.intent.action.LOCKED_BOOT_COMPLETED\" />\n                <action android:name=\"android.intent.action.MY_PACKAGE_REPLACED\" />\n            </intent-filter>\n        </receiver>\n\n        <service\n            android:name=\"io.nekohasekai.sagernet.bg.ForegroundDetectorService\"\n            android:exported=\"true\"\n            android:permission=\"android.permission.BIND_ACCESSIBILITY_SERVICE\"\n            android:process=\":bg\">\n            <intent-filter>\n                <action android:name=\"android.accessibilityservice.AccessibilityService\" />\n            </intent-filter>\n            <meta-data\n                android:name=\"android.accessibilityservice\"\n                android:resource=\"@xml/foreground_detector_service\" />\n        </service>\n\n        <provider\n            android:name=\"androidx.startup.InitializationProvider\"\n            android:authorities=\"${applicationId}.androidx-startup\"\n            tools:node=\"remove\" />\n\n        <activity\n            android:name=\"com.mikepenz.aboutlibraries.ui.LibsActivity\"\n            tools:node=\"remove\" />\n\n        <activity\n            android:name=\"com.journeyapps.barcodescanner.CaptureActivity\"\n            tools:node=\"remove\" />\n\n        <service\n            android:name=\"androidx.room.MultiInstanceInvalidationService\"\n            android:process=\":bg\" />\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/aidl/io/nekohasekai/sagernet/aidl/AppStatsList.aidl",
    "content": "package io.nekohasekai.sagernet.aidl;\n\nparcelable AppStatsList;\n"
  },
  {
    "path": "app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetService.aidl",
    "content": "package io.nekohasekai.sagernet.aidl;\n\nimport io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback;\n\ninterface ISagerNetService {\n  int getState();\n  String getProfileName();\n\n  void registerCallback(in ISagerNetServiceCallback cb);\n  void startListeningForBandwidth(in ISagerNetServiceCallback cb, long timeout);\n  oneway void stopListeningForBandwidth(in ISagerNetServiceCallback cb);\n  void startListeningForStats(in ISagerNetServiceCallback cb, long timeout);\n  oneway void stopListeningForStats(in ISagerNetServiceCallback cb);\n  oneway void unregisterCallback(in ISagerNetServiceCallback cb);\n  oneway void protect(int fd);\n  int urlTest();\n  oneway void resetTrafficStats();\n  boolean getTrafficStatsEnabled();\n\n}\n"
  },
  {
    "path": "app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl",
    "content": "package io.nekohasekai.sagernet.aidl;\n\nimport io.nekohasekai.sagernet.aidl.TrafficStats;\nimport io.nekohasekai.sagernet.aidl.AppStatsList;\n\noneway interface ISagerNetServiceCallback {\n  void stateChanged(int state, String profileName, String msg);\n  void trafficUpdated(long profileId, in TrafficStats stats, boolean isCurrent);\n  void statsUpdated(in AppStatsList statsList);\n  void observatoryResultsUpdated(long groupId);\n  // Traffic data has persisted to database, listener should refetch their data from database\n  void profilePersisted(long profileId);\n  void missingPlugin(String profileName, String pluginName);\n  void routeAlert(int type, String routeName);\n}\n"
  },
  {
    "path": "app/src/main/aidl/io/nekohasekai/sagernet/aidl/TrafficStats.aidl",
    "content": "package io.nekohasekai.sagernet.aidl;\n\nparcelable TrafficStats;\n"
  },
  {
    "path": "app/src/main/assets/LICENSE",
    "content": "Copyright (C) 2021 by nekohasekai\n<contact-sagernet@sekai.icu>\n\nThis program is free software: you can\nredistribute it and/or modify it under\nthe terms of the GNU General Public License\nas published by the Free Software Foundation,\neither version 3 of the License,\nor (at your option) any later version.\n\nThis program is distributed in the hope\nthat it will be useful, but WITHOUT ANY WARRANTY;\nwithout even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\nSee the GNU General Public License for more details.\n\nYou should have received a copy of the\nGNU General Public License along with this program.\nIf not, see <http://www.gnu.org/licenses/>."
  },
  {
    "path": "app/src/main/assets/terminal.properties",
    "content": "# https://github.com/chriskempson/base16-xresources/blob/master/base16-google.light.256.xresources\n# Base16 Google\n# Scheme: Seth Wright (http://sethawright.com)\nforeground=#373b41\nbackground=#ffffff\ncursor=#373b41\n\ncolor0=#1d1f21\ncolor1=#CC342B\ncolor2=#198844\ncolor3=#FBA922\ncolor4=#3971ED\ncolor5=#A36AC7\ncolor6=#3971ED\ncolor7=#c5c8c6\ncolor8=#969896\ncolor9=#CC342B\ncolor10=#198844\ncolor11=#FBA922\ncolor12=#3971ED\ncolor13=#A36AC7\ncolor14=#3971ED\ncolor15=#ffffff\n\ncolor16=#F96A38\ncolor17=#3971ED\ncolor18=#282a2e\ncolor19=#373b41\ncolor20=#b4b7b4\ncolor21=#e0e0e0\n"
  },
  {
    "path": "app/src/main/java/cn/hutool/cache/impl/AbstractCacheWithoutLock.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage cn.hutool.cache.impl;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\nimport cn.hutool.cache.Cache;\nimport cn.hutool.cache.CacheListener;\nimport cn.hutool.core.collection.CopiedIter;\nimport cn.hutool.core.lang.func.Func0;\n\n/**\n * 超时和限制大小的缓存的默认实现<br>\n * 继承此抽象缓存需要：<br>\n * <ul>\n * <li>创建一个新的Map</li>\n * <li>实现 {@code prune} 策略</li>\n * </ul>\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly, jodd\n */\npublic abstract class AbstractCacheWithoutLock<K, V> implements Cache<K, V> {\n    private static final long serialVersionUID = 1L;\n\n    protected Map<K, CacheObj<K, V>> cacheMap;\n\n    /**\n     * 写的时候每个key一把锁，降低锁的粒度\n     */\n    protected final Map<K, Lock> keyLockMap = new ConcurrentHashMap<>();\n\n    /**\n     * 返回缓存容量，{@code 0}表示无大小限制\n     */\n    protected int capacity;\n    /**\n     * 缓存失效时长， {@code 0} 表示无限制，单位毫秒\n     */\n    protected long timeout;\n\n    /**\n     * 每个对象是否有单独的失效时长，用于决定清理过期对象是否有必要。\n     */\n    protected boolean existCustomTimeout;\n\n    /**\n     * 命中数，即命中缓存计数\n     */\n    protected AtomicLong hitCount = new AtomicLong();\n    /**\n     * 丢失数，即未命中缓存计数\n     */\n    protected AtomicLong missCount = new AtomicLong();\n\n    /**\n     * 缓存监听\n     */\n    protected CacheListener<K, V> listener;\n\n    // ---------------------------------------------------------------- put start\n    @Override\n    public void put(K key, V object) {\n        put(key, object, timeout);\n    }\n\n    @Override\n    public void put(K key, V object, long timeout) {\n        putWithoutLock(key, object, timeout);\n    }\n\n    /**\n     * 加入元素，无锁\n     *\n     * @param key     键\n     * @param object  值\n     * @param timeout 超时时长\n     * @since 4.5.16\n     */\n    public void putWithoutLock(K key, V object, long timeout) {\n        CacheObj<K, V> co = new CacheObj<>(key, object, timeout);\n        if (timeout != 0) {\n            existCustomTimeout = true;\n        }\n        if (isFull()) {\n            pruneCache();\n        }\n        cacheMap.put(key, co);\n    }\n    // ---------------------------------------------------------------- put end\n\n    // ---------------------------------------------------------------- get start\n    @Override\n    public boolean containsKey(K key) {\n        // 不存在或已移除\n        final CacheObj<K, V> co = cacheMap.get(key);\n        if (co == null) {\n            return false;\n        }\n\n        if (false == co.isExpired()) {\n            // 命中\n            return true;\n        }\n\n        // 过期\n        remove(key, true);\n        return false;\n    }\n\n    /**\n     * @return 命中数\n     */\n    public long getHitCount() {\n        return hitCount.get();\n    }\n\n    /**\n     * @return 丢失数\n     */\n    public long getMissCount() {\n        return missCount.get();\n    }\n\n    @Override\n    public V get(K key, boolean isUpdateLastAccess, Func0<V> supplier) {\n        V v = get(key, isUpdateLastAccess);\n        if (null == v && null != supplier) {\n            //每个key单独获取一把锁，降低锁的粒度提高并发能力，see pr#1385@Github\n            final Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock());\n            keyLock.lock();\n            try {\n                // 双重检查锁，防止在竞争锁的过程中已经有其它线程写入\n                final CacheObj<K, V> co = cacheMap.get(key);\n                if (null == co || co.isExpired()) {\n                    try {\n                        v = supplier.call();\n                    } catch (Exception e) {\n                        throw new RuntimeException(e);\n                    }\n                    put(key, v, this.timeout);\n                } else {\n                    v = co.get(isUpdateLastAccess);\n                }\n            } finally {\n                keyLock.unlock();\n                keyLockMap.remove(key);\n            }\n        }\n        return v;\n    }\n\n    @Override\n    public V get(K key, boolean isUpdateLastAccess) {\n        // 尝试读取缓存，使用乐观读锁\n        CacheObj<K, V> co = cacheMap.get(key);\n\n        // 未命中\n        if (null == co) {\n            missCount.incrementAndGet();\n            return null;\n        } else if (false == co.isExpired()) {\n            hitCount.incrementAndGet();\n            return co.get(isUpdateLastAccess);\n        }\n\n        // 过期，既不算命中也不算非命中\n        remove(key, true);\n        return null;\n    }\n\n    // ---------------------------------------------------------------- get end\n\n    @Override\n    public Iterator<V> iterator() {\n        CacheObjIterator<K, V> copiedIterator = (CacheObjIterator<K, V>) this.cacheObjIterator();\n        return new CacheValuesIterator<>(copiedIterator);\n    }\n\n    @Override\n    public Iterator<CacheObj<K, V>> cacheObjIterator() {\n        CopiedIter<CacheObj<K, V>> copiedIterator;\n        copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator());\n        return new CacheObjIterator<>(copiedIterator);\n    }\n\n    // ---------------------------------------------------------------- prune start\n\n    /**\n     * 清理实现<br>\n     * 子类实现此方法时无需加锁\n     *\n     * @return 清理数\n     */\n    protected abstract int pruneCache();\n\n    @Override\n    public final int prune() {\n        return pruneCache();\n    }\n    // ---------------------------------------------------------------- prune end\n\n    // ---------------------------------------------------------------- common start\n    @Override\n    public int capacity() {\n        return capacity;\n    }\n\n    /**\n     * @return 默认缓存失效时长。<br>\n     * 每个对象可以单独设置失效时长\n     */\n    @Override\n    public long timeout() {\n        return timeout;\n    }\n\n    /**\n     * 只有设置公共缓存失效时长或每个对象单独的失效时长时清理可用\n     *\n     * @return 过期对象清理是否可用，内部使用\n     */\n    protected boolean isPruneExpiredActive() {\n        return (timeout != 0) || existCustomTimeout;\n    }\n\n    @Override\n    public boolean isFull() {\n        return (capacity > 0) && (cacheMap.size() >= capacity);\n    }\n\n    @Override\n    public void remove(K key) {\n        remove(key, false);\n    }\n\n    @Override\n    public void clear() {\n        cacheMap.clear();\n    }\n\n    @Override\n    public int size() {\n        return cacheMap.size();\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return cacheMap.isEmpty();\n    }\n\n    @Override\n    public String toString() {\n        return this.cacheMap.toString();\n    }\n    // ---------------------------------------------------------------- common end\n\n    /**\n     * 设置监听\n     *\n     * @param listener 监听\n     * @return this\n     * @since 5.5.2\n     */\n    @Override\n    public AbstractCacheWithoutLock<K, V> setListener(CacheListener<K, V> listener) {\n        this.listener = listener;\n        return this;\n    }\n\n    /**\n     * 返回所有键\n     *\n     * @return 所有键\n     * @since 5.5.9\n     */\n    public Set<K> keySet() {\n        return this.cacheMap.keySet();\n    }\n\n    /**\n     * 对象移除回调。默认无动作<br>\n     * 子类可重写此方法用于监听移除事件，如果重写，listener将无效\n     *\n     * @param key          键\n     * @param cachedObject 被缓存的对象\n     */\n    protected void onRemove(K key, V cachedObject) {\n        final CacheListener<K, V> listener = this.listener;\n        if (null != listener) {\n            listener.onRemove(key, cachedObject);\n        }\n    }\n\n    /**\n     * 移除key对应的对象\n     *\n     * @param key           键\n     * @param withMissCount 是否计数丢失数\n     */\n    private void remove(K key, boolean withMissCount) {\n        CacheObj<K, V> co = removeWithoutLock(key, withMissCount);\n        if (null != co) {\n            onRemove(co.key, co.obj);\n        }\n    }\n\n    /**\n     * 移除key对应的对象，不加锁\n     *\n     * @param key           键\n     * @param withMissCount 是否计数丢失数\n     * @return 移除的对象，无返回null\n     */\n    private CacheObj<K, V> removeWithoutLock(K key, boolean withMissCount) {\n        final CacheObj<K, V> co = cacheMap.remove(key);\n        if (withMissCount) {\n            // 在丢失计数有效的情况下，移除一般为get时的超时操作，此处应该丢失数+1\n            this.missCount.incrementAndGet();\n        }\n        return co;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/cn/hutool/cache/impl/LFUCacheCompact.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n\npackage cn.hutool.cache.impl;\n\nimport android.os.Build;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport cn.hutool.cache.Cache;\n\npublic class LFUCacheCompact<K, V> {\n\n    protected int capacity;\n    protected long timeout;\n\n    public LFUCacheCompact(int capacity, long timeout) {\n        if (Integer.MAX_VALUE == capacity) {\n            capacity -= 1;\n        }\n\n        this.capacity = capacity;\n        this.timeout = timeout;\n    }\n\n    protected void onRemove(K key, V cachedObject) {\n    }\n\n    public Cache<K, V> build(boolean async) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            return new LFUCache<K, V>(capacity, timeout) {\n                {\n                    if (async) {\n                        cacheMap = new ConcurrentHashMap<>();\n                    }\n                }\n\n                @Override\n                protected void onRemove(K key, V cachedObject) {\n                    LFUCacheCompact.this.onRemove(key, cachedObject);\n                }\n            };\n        } else {\n            return new LFUCacheWithoutLock<K, V>(capacity, timeout) {\n                @Override\n                protected Map<K, CacheObj<K, V>> createCacheMap() {\n                    return new ConcurrentHashMap<>();\n                }\n\n                @Override\n                protected void onRemove(K key, V cachedObject) {\n                    LFUCacheCompact.this.onRemove(key, cachedObject);\n                }\n            };\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/cn/hutool/cache/impl/LFUCacheWithoutLock.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage cn.hutool.cache.impl;\n\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * LFU(least frequently used) 最少使用率缓存<br>\n * 根据使用次数来判定对象是否被持续缓存<br>\n * 使用率是通过访问次数计算的。<br>\n * 当缓存满时清理过期对象。<br>\n * 清理后依旧满的情况下清除最少访问（访问计数最小）的对象并将其他对象的访问数减去这个最小访问数，以便新对象进入后可以公平计数。\n *\n * @param <K> 键类型\n * @param <V> 值类型\n * @author Looly, jodd\n */\npublic class LFUCacheWithoutLock<K, V> extends AbstractCacheWithoutLock<K, V> {\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * 构造\n     *\n     * @param capacity 容量\n     */\n    public LFUCacheWithoutLock(int capacity) {\n        this(capacity, 0);\n    }\n\n    /**\n     * 构造\n     *\n     * @param capacity 容量\n     * @param timeout  过期时长\n     */\n    public LFUCacheWithoutLock(int capacity, long timeout) {\n        if (Integer.MAX_VALUE == capacity) {\n            capacity -= 1;\n        }\n\n        this.capacity = capacity;\n        this.timeout = timeout;\n        cacheMap = createCacheMap();\n    }\n\n    protected Map<K, CacheObj<K, V>> createCacheMap() {\n        return new HashMap<>(capacity + 1, 1.0f);\n    }\n\n    // ---------------------------------------------------------------- prune\n\n    /**\n     * 清理过期对象。<br>\n     * 清理后依旧满的情况下清除最少访问（访问计数最小）的对象并将其他对象的访问数减去这个最小访问数，以便新对象进入后可以公平计数。\n     *\n     * @return 清理个数\n     */\n    @Override\n    protected int pruneCache() {\n        int count = 0;\n        CacheObj<K, V> comin = null;\n\n        // 清理过期对象并找出访问最少的对象\n        Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();\n        CacheObj<K, V> co;\n        while (values.hasNext()) {\n            co = values.next();\n            if (co.isExpired() == true) {\n                values.remove();\n                onRemove(co.key, co.obj);\n                count++;\n                continue;\n            }\n\n            //找出访问最少的对象\n            if (comin == null || co.accessCount.get() < comin.accessCount.get()) {\n                comin = co;\n            }\n        }\n\n        // 减少所有对象访问量，并清除减少后为0的访问对象\n        if (isFull() && comin != null) {\n            long minAccessCount = comin.accessCount.get();\n\n            values = cacheMap.values().iterator();\n            CacheObj<K, V> co1;\n            while (values.hasNext()) {\n                co1 = values.next();\n                if (co1.accessCount.addAndGet(-minAccessCount) <= 0) {\n                    values.remove();\n                    onRemove(co1.key, co1.obj);\n                    count++;\n                }\n            }\n        }\n\n        return count;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/ConfigurationActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.app.Activity\nimport android.content.Intent\n\n/**\n * Base class for configuration activity. A configuration activity is started when user wishes to configure the\n * selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke\n * `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:\n *\n * <pre class=\"prettyprint\">&lt;manifest&gt;\n *    ...\n *    &lt;application&gt;\n *        ...\n *        &lt;activity android:name=\".ConfigureActivity\"&gt;\n *            &lt;intent-filter&gt;\n *                &lt;action android:name=\"com.github.shadowsocks.plugin.ACTION_CONFIGURE\"/&gt;\n *                &lt;category android:name=\"android.intent.category.DEFAULT\"/&gt;\n *                &lt;data android:scheme=\"plugin\"\n *                         android:host=\"com.github.shadowsocks\"\n *                         android:path=\"/$PLUGIN_ID\"/&gt;\n *            &lt;/intent-filter&gt;\n *        &lt;/activity&gt;\n *        ...\n *    &lt;/application&gt;\n *&lt;/manifest&gt;</pre>\n */\nabstract class ConfigurationActivity : OptionsCapableActivity() {\n    /**\n     * Equivalent to setResult(RESULT_CANCELED).\n     */\n    fun discardChanges() = setResult(Activity.RESULT_CANCELED)\n\n    /**\n     * Equivalent to setResult(RESULT_OK, args_with_correct_format).\n     *\n     * @param options PluginOptions to save.\n     */\n    fun saveChanges(options: PluginOptions) =\n            setResult(Activity.RESULT_OK, Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString()))\n\n    /**\n     * Finish this activity and request manual editor to pop up instead.\n     */\n    fun fallbackToManualEditor() {\n        setResult(PluginContract.RESULT_FALLBACK)\n        finish()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/HelpActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\n/**\n * Base class for a help activity. A help activity is started when user taps help when configuring options for your\n * plugin. To create a help activity, just extend this class, and add it to your manifest like this:\n *\n * <pre class=\"prettyprint\">&lt;manifest&gt;\n *    ...\n *    &lt;application&gt;\n *        ...\n *        &lt;activity android:name=\".HelpActivity\"&gt;\n *            &lt;intent-filter&gt;\n *                &lt;action android:name=\"com.github.shadowsocks.plugin.ACTION_HELP\"/&gt;\n *                &lt;category android:name=\"android.intent.category.DEFAULT\"/&gt;\n *                &lt;data android:scheme=\"plugin\"\n *                         android:host=\"com.github.shadowsocks\"\n *                         android:path=\"/$PLUGIN_ID\"/&gt;\n *            &lt;/intent-filter&gt;\n *        &lt;/activity&gt;\n *        ...\n *    &lt;/application&gt;\n *&lt;/manifest&gt;</pre>\n */\nabstract class HelpActivity : OptionsCapableActivity() {\n    override fun onInitializePluginOptions(options: PluginOptions) { }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/HelpCallback.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.content.Intent\n\n/**\n * HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to\n * provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your\n * manifest following the same procedure as adding a HelpActivity.\n */\nabstract class HelpCallback : HelpActivity() {\n    abstract fun produceHelpMessage(options: PluginOptions): CharSequence\n\n    override fun onInitializePluginOptions(options: PluginOptions) {\n        setResult(RESULT_OK, Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))\n        finish()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/InternalPlugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nclass InternalPlugin(override val id: String, override val label: CharSequence) : Plugin() {\n    companion object {\n        val SIMPLE_OBFS = InternalPlugin(\"obfs-local\", \"Simple Obfs (Internal)\")\n        val V2RAY_PLUGIN = InternalPlugin(\"v2ray-plugin\", \"V2Ray Plugin (Internal)\")\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.content.pm.ResolveInfo\n\nclass NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {\n    init {\n        check(resolveInfo.providerInfo != null)\n    }\n\n    override val componentInfo get() = resolveInfo.providerInfo!!\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.content.ContentProvider\nimport android.content.ContentValues\nimport android.database.Cursor\nimport android.database.MatrixCursor\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.ParcelFileDescriptor\nimport androidx.core.os.bundleOf\n\n/**\n * Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required\n * to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this\n * class, implement the abstract methods, and add it to your manifest like this:\n *\n * <pre class=\"prettyprint\">&lt;manifest&gt;\n *    ...\n *    &lt;application&gt;\n *        ...\n *        &lt;provider android:name=\"com.github.shadowsocks.$PLUGIN_ID.BinaryProvider\"\n *                     android:authorities=\"com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider\"&gt;\n *            &lt;intent-filter&gt;\n *                &lt;category android:name=\"com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN\" /&gt;\n *            &lt;/intent-filter&gt;\n *        &lt;/provider&gt;\n *        ...\n *    &lt;/application&gt;\n *&lt;/manifest&gt;</pre>\n */\nabstract class NativePluginProvider : ContentProvider() {\n    override fun getType(uri: Uri): String? = \"application/x-elf\"\n\n    override fun onCreate(): Boolean = true\n\n    /**\n     * Provide all files needed for native plugin.\n     *\n     * @param provider A helper object to use to add files.\n     */\n    protected abstract fun populateFiles(provider: PathProvider)\n\n    override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,\n                       sortOrder: String?): Cursor? {\n        check(selection == null && selectionArgs == null && sortOrder == null)\n        val result = MatrixCursor(projection)\n        populateFiles(PathProvider(uri, result))\n        return result\n    }\n\n    /**\n     * Returns executable entry absolute path.\n     * This is used for fast mode initialization where ss-local launches your native binary at the path given directly.\n     * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:\n     *  - android:installLocation=\"internalOnly\" for <manifest>\n     *  - android:extractNativeLibs=\"true\" for <application>\n     *\n     * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the\n     * default behavior.\n     *\n     * @return Absolute path for executable entry.\n     */\n    open fun getExecutable(): String = throw UnsupportedOperationException()\n\n    abstract fun openFile(uri: Uri): ParcelFileDescriptor\n    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {\n        check(mode == \"r\")\n        return openFile(uri)\n    }\n\n    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {\n        PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))\n        else -> super.call(method, arg, extras)\n    }\n\n    // Methods that should not be used\n    override fun insert(uri: Uri, values: ContentValues?): Uri? = throw UnsupportedOperationException()\n    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =\n            throw UnsupportedOperationException()\n    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =\n            throw UnsupportedOperationException()\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\n\n\nobject NoPlugin : Plugin() {\n    override val id: String get() = \"\"\n    override val label: CharSequence get() = SagerNet.application.getText(R.string.plugin_disabled)\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport io.nekohasekai.sagernet.ui.ThemedActivity\n\n/**\n * Activity that's capable of getting EXTRA_OPTIONS input.\n */\nabstract class OptionsCapableActivity : ThemedActivity() {\n    protected fun pluginOptions(intent: Intent = this.intent) = try {\n        PluginOptions(\"\", intent.getStringExtra(PluginContract.EXTRA_OPTIONS))\n    } catch (exc: IllegalArgumentException) {\n        Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()\n        PluginOptions()\n    }\n\n    /**\n     * Populate args to your user interface.\n     *\n     * @param options PluginOptions parsed.\n     */\n    protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions())\n\n    override fun onPostCreate(savedInstanceState: Bundle?) {\n        super.onPostCreate(savedInstanceState)\n        if (savedInstanceState == null) onInitializePluginOptions()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.database.MatrixCursor\nimport android.net.Uri\nimport java.io.File\n\n/**\n * Helper class to provide relative paths of files to copy.\n */\nclass PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {\n    private val basePath = baseUri.path?.trim('/') ?: \"\"\n\n    fun addPath(path: String, mode: Int = 0b110100100): PathProvider {\n        val trimmed = path.trim('/')\n        if (trimmed.startsWith(basePath)) cursor.newRow()\n                .add(PluginContract.COLUMN_PATH, trimmed)\n                .add(PluginContract.COLUMN_MODE, mode)\n        return this\n    }\n    fun addTo(file: File, to: String = \"\", mode: Int = 0b110100100): PathProvider {\n        var sub = to + file.name\n        if (basePath.startsWith(sub)) if (file.isDirectory) {\n            sub += '/'\n            file.listFiles()!!.forEach { addTo(it, sub, mode) }\n        } else addPath(sub, mode)\n        return this\n    }\n    fun addAt(file: File, at: String = \"\", mode: Int = 0b110100100): PathProvider {\n        if (basePath.startsWith(at)) {\n            if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode)\n        }\n        return this\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/Plugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.graphics.drawable.Drawable\n\nabstract class Plugin {\n    abstract val id: String\n    open val idAliases get() = emptyArray<String>()\n    abstract val label: CharSequence\n    open val icon: Drawable? get() = null\n    open val defaultConfig: String? get() = null\n    open val packageName: String get() = \"\"\n    open val trusted: Boolean get() = true\n    open val directBootAware: Boolean get() = true\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n        return id == (other as Plugin).id\n    }\n    override fun hashCode() = id.hashCode()\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.utils.Commandline\nimport java.util.*\n\nclass PluginConfiguration(val pluginsOptions: MutableMap<String, PluginOptions>, var selected: String) {\n    private constructor(plugins: List<PluginOptions>) : this(\n            plugins.filter { it.id.isNotEmpty() }.associateBy { it.id }.toMutableMap(),\n            if (plugins.isEmpty()) \"\" else plugins[0].id)\n    constructor(): this(listOf())\n    constructor(plugin: String) : this(plugin.split('\\n').map { line ->\n        if (line.startsWith(\"kcptun \")) {\n            val opt = PluginOptions()\n            opt.id = \"kcptun\"\n            try {\n                val iterator = Commandline.translateCommandline(line).drop(1).iterator()\n                while (iterator.hasNext()) {\n                    val option = iterator.next()\n                    when {\n                        option == \"--nocomp\" -> opt[\"nocomp\"] = null\n                        option.startsWith(\"--\") -> opt[option.substring(2)] = iterator.next()\n                        else -> throw IllegalArgumentException(\"Unknown kcptun parameter: $option\")\n                    }\n                }\n            } catch (exc: Exception) {\n                Logs.w(exc)\n            }\n            opt\n        } else PluginOptions(line)\n    })\n\n\n    fun getOptions(\n            id: String = selected,\n            defaultConfig: () -> String? = { PluginManager.fetchPlugins(true).lookup[id]?.defaultConfig }\n    ) = if (id.isEmpty()) PluginOptions() else pluginsOptions[id] ?: PluginOptions(id, defaultConfig())\n\n    override fun toString(): String {\n        val result = LinkedList<PluginOptions>()\n        for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)\n        if (!pluginsOptions.contains(selected)) result.addFirst(getOptions())\n        return result.joinToString(\"\\n\") { it.toString(false) }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\n/**\n * The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.\n *\n * This class is written in Java to keep Java interoperability.\n */\nobject PluginContract {\n    /**\n     * ContentProvider Action: Used for NativePluginProvider.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN\"\n     */\n    const val ACTION_NATIVE_PLUGIN = \"com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN\"\n\n    /**\n     * Activity Action: Used for ConfigurationActivity.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.ACTION_CONFIGURE\"\n     */\n    const val ACTION_CONFIGURE = \"com.github.shadowsocks.plugin.ACTION_CONFIGURE\"\n    /**\n     * Activity Action: Used for HelpActivity or HelpCallback.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.ACTION_HELP\"\n     */\n    const val ACTION_HELP = \"com.github.shadowsocks.plugin.ACTION_HELP\"\n\n    /**\n     * The lookup key for a string that provides the plugin entry binary.\n     *\n     * Example: \"/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so\"\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.EXTRA_ENTRY\"\n     */\n    const val EXTRA_ENTRY = \"com.github.shadowsocks.plugin.EXTRA_ENTRY\"\n    /**\n     * The lookup key for a string that provides the options as a string.\n     *\n     * Example: \"obfs=http;obfs-host=www.baidu.com\"\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.EXTRA_OPTIONS\"\n     */\n    const val EXTRA_OPTIONS = \"com.github.shadowsocks.plugin.EXTRA_OPTIONS\"\n    /**\n     * The lookup key for a CharSequence that provides user relevant help message.\n     *\n     * Example: \"obfs=<http></http>|tls>            Enable obfuscating: HTTP or TLS (Experimental).\n     * obfs-host=<host_name>      Hostname for obfuscating (Experimental).\"\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE\"\n    </host_name> */\n    const val EXTRA_HELP_MESSAGE = \"com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE\"\n\n    /**\n     * The metadata key to retrieve plugin version. Required for plugin applications.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.version\"\n     */\n    const val METADATA_KEY_VERSION = \"com.github.shadowsocks.plugin.version\"\n\n    /**\n     * The metadata key to retrieve plugin id. Required for plugins.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.id\"\n     */\n    const val METADATA_KEY_ID = \"com.github.shadowsocks.plugin.id\"\n    /**\n     * The metadata key to retrieve plugin id aliases.\n     * Can be a string (representing one alias) or a resource to a string or string array.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.id.aliases\"\n     */\n    const val METADATA_KEY_ID_ALIASES = \"com.github.shadowsocks.plugin.id.aliases\"\n    /**\n     * The metadata key to retrieve default configuration. Default value is empty.\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.default_config\"\n     */\n    const val METADATA_KEY_DEFAULT_CONFIG = \"com.github.shadowsocks.plugin.default_config\"\n    /**\n     * The metadata key to retrieve executable path to your native binary.\n     * This path should be relative to your application's nativeLibraryDir.\n     *\n     * If this is set, the host app will prefer this value and (probably) not launch your app at all (aka faster mode).\n     * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:\n     *  - android:installLocation=\"internalOnly\" for <manifest>\n     *  - android:extractNativeLibs=\"true\" for <application>\n     *\n     * Do not use this if you plan to do some setup work before giving away your binary path,\n     *  or your native binary is not at a fixed location relative to your application's nativeLibraryDir.\n     *\n     * Since plugin lib: 1.3.0\n     *\n     * Constant Value: \"com.github.shadowsocks.plugin.executable_path\"\n     */\n    const val METADATA_KEY_EXECUTABLE_PATH = \"com.github.shadowsocks.plugin.executable_path\"\n\n    const val METHOD_GET_EXECUTABLE = \"shadowsocks:getExecutable\"\n\n    /** ConfigurationActivity result: fallback to manual edit mode.  */\n    const val RESULT_FALLBACK = 1\n\n    /**\n     * Relative to the file to be copied. This column is required.\n     *\n     * Example: \"kcptun\", \"doc/help.txt\"\n     *\n     * Type: String\n     */\n    const val COLUMN_PATH = \"path\"\n    /**\n     * File mode bits. Default value is 644 in octal.\n     *\n     * Example: 0b110100100 (for 755 in octal)\n     *\n     * Type: Int or String (deprecated)\n     */\n    const val COLUMN_MODE = \"mode\"\n\n    /**\n     * The scheme for general plugin actions.\n     */\n    const val SCHEME = \"plugin\"\n    /**\n     * The authority for general plugin actions.\n     */\n    const val AUTHORITY = \"com.github.shadowsocks\"\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/PluginList.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.widget.Toast\nimport io.nekohasekai.sagernet.SagerNet\n\nclass PluginList(skipInternal: Boolean) : ArrayList<Plugin>() {\n    init {\n        add(NoPlugin)\n        if (!skipInternal) {\n            add(InternalPlugin.SIMPLE_OBFS)\n            add(InternalPlugin.V2RAY_PLUGIN)\n        }\n        addAll(SagerNet.application.packageManager.queryIntentContentProviders(\n                Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)\n                .filter { it.providerInfo.exported }.map { NativePlugin(it) })\n    }\n\n    val lookup = mutableMapOf<String, Plugin>().apply {\n        for (plugin in this@PluginList.toList()) {\n            fun check(old: Plugin?) {\n                if (old != null && old != plugin) {\n                    this@PluginList.remove(old)\n                }\n                // skip check\n                /*if (old != null && old !== plugin) {\n                    val packages = this@PluginList.filter { it.id == plugin.id }.joinToString { it.packageName }\n                    val message = \"Conflicting plugins found from: $packages\"\n                    Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()\n                    throw IllegalStateException(message)\n                }*/\n            }\n            check(put(plugin.id, plugin))\n            for (alias in plugin.idAliases) check(put(alias, plugin))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.annotation.SuppressLint\nimport android.content.BroadcastReceiver\nimport android.content.ContentResolver\nimport android.content.Intent\nimport android.content.pm.ComponentInfo\nimport android.content.pm.PackageManager\nimport android.content.pm.ProviderInfo\nimport android.content.pm.Signature\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Build\nimport android.system.Os\nimport android.util.Base64\nimport android.widget.Toast\nimport androidx.core.os.bundleOf\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.listenForPackageChanges\nimport io.nekohasekai.sagernet.ktx.signaturesCompat\nimport java.io.File\nimport java.io.FileNotFoundException\n\nobject PluginManager {\n    class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin),\n            BaseService.ExpectedException {\n        override fun getLocalizedMessage() = SagerNet.application.getString(R.string.plugin_unknown, plugin)\n    }\n\n    /**\n     * Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes\n     * to publish or has published plugins for this app. You can obtain your public key by executing:\n     *\n     * $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc\n     *\n     * If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your\n     * public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.\n     */\n    val trustedSignatures by lazy {\n        SagerNet.packageInfo.signaturesCompat.toSet() +\n                Signature(Base64.decode(  // @Mygod\n                \"\"\"\n                    |MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD\n                    |TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv\n                    |ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx\n                    |OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln\n                    |b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln\n                    |b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\n                    |AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0\n                    |L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS\n                    |Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL\n                    |M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX\n                    |YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw\n                    |agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho\n                    |6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l\n                    |YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO\n                    |zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY\n                    |RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v\n                    |wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc\n                    |1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=\n                  \"\"\", Base64.DEFAULT)) +\n                Signature(Base64.decode( // @madeye\n                \"\"\"\n                    |MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE\n                    |CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV\n                    |BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow\n                    |ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV\n                    |BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG\n                    |9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr\n                    |4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu\n                    |AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ\n                    |Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n\n                    |AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI\n                    |1iNuLIssdhDz3IL9Yg==\n                  \"\"\", Base64.DEFAULT))\n    }\n\n    private var receiver: BroadcastReceiver? = null\n    private var cachedPlugins: PluginList? = null\n    private var cachedPluginsSkipInternal: PluginList? = null\n    fun fetchPlugins(skipInternal: Boolean) = synchronized(this) {\n        if (receiver == null) receiver = SagerNet.application.listenForPackageChanges {\n            synchronized(this) {\n                receiver = null\n                cachedPlugins = null\n                cachedPluginsSkipInternal = null\n            }\n        }\n        if (skipInternal) {\n            if (cachedPlugins == null) cachedPlugins = PluginList(skipInternal)\n            cachedPlugins!!\n        } else {\n            if (cachedPluginsSkipInternal == null) cachedPluginsSkipInternal = PluginList(skipInternal)\n            cachedPluginsSkipInternal!!\n        }\n    }\n\n    private fun buildUri(id: String) = Uri.Builder()\n            .scheme(PluginContract.SCHEME)\n            .authority(PluginContract.AUTHORITY)\n            .path(\"/$id\")\n            .build()\n    fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))\n\n    data class InitResult(\n            val path: String,\n            val options: PluginOptions,\n            val isV2: Boolean = false,\n    )\n\n    // the following parts are meant to be used by :bg\n    @Throws(Throwable::class)\n    fun init(configuration: PluginConfiguration): InitResult? {\n        if (configuration.selected.isEmpty()) return null\n        var throwable: Throwable? = null\n\n        try {\n            val result = initNative(configuration)\n            if (result != null) return result\n        } catch (t: Throwable) {\n            if (throwable == null) throwable = t else Logs.w(t)\n        }\n\n        // add other plugin types here\n\n        throw throwable ?: PluginNotFoundException(configuration.selected)\n    }\n\n    private fun initNative(configuration: PluginConfiguration): InitResult? {\n        var flags = PackageManager.GET_META_DATA\n        if (Build.VERSION.SDK_INT >= 24) {\n            flags = flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE\n        }\n        val providers = SagerNet.application.packageManager.queryIntentContentProviders(\n                Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(configuration.selected)), flags)\n                .filter { it.providerInfo.exported }\n        if (providers.isEmpty()) return null\n        if (providers.size > 1) {\n            val message = \"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}\"\n            Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()\n            throw IllegalStateException(message)\n        }\n        val provider = providers.single().providerInfo\n        val options = configuration.getOptions { provider.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }\n        val isV2 = provider.applicationInfo.metaData?.getString(PluginContract.METADATA_KEY_VERSION)\n                ?.substringBefore('.')?.toIntOrNull() ?: 0 >= 2\n        var failure: Throwable? = null\n        try {\n            initNativeFaster(provider)?.also { return InitResult(it, options, isV2) }\n        } catch (t: Throwable) {\n            Logs.w(\"Initializing native plugin faster mode failed\")\n            failure = t\n        }\n\n        val uri = Uri.Builder().apply {\n            scheme(ContentResolver.SCHEME_CONTENT)\n            authority(provider.authority)\n        }.build()\n        try {\n            return initNativeFast(SagerNet.application.contentResolver, options, uri)?.let { InitResult(it, options, isV2) }\n        } catch (t: Throwable) {\n            Logs.w(\"Initializing native plugin fast mode failed\")\n            failure?.also { t.addSuppressed(it) }\n            failure = t\n        }\n\n        try {\n            return initNativeSlow(SagerNet.application.contentResolver, options, uri)?.let { InitResult(it, options, isV2) }\n        } catch (t: Throwable) {\n            failure?.also { t.addSuppressed(it) }\n            throw t\n        }\n    }\n\n    private fun initNativeFaster(provider: ProviderInfo): String? {\n        return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)?.let { relativePath ->\n            File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {\n                check(canExecute())\n            }.absolutePath\n        }\n    }\n\n    private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {\n        return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null,\n                bundleOf(PluginContract.EXTRA_OPTIONS to options.id))?.getString(PluginContract.EXTRA_ENTRY)?.also {\n            check(File(it).canExecute())\n        }\n    }\n\n    @SuppressLint(\"Recycle\")\n    private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {\n        var initialized = false\n        fun entryNotFound(): Nothing = throw IndexOutOfBoundsException(\"Plugin entry binary not found\")\n        val pluginDir = File(SagerNet.deviceStorage.noBackupFilesDir, \"plugin\")\n        (cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)\n                ?: return null).use { cursor ->\n            if (!cursor.moveToFirst()) entryNotFound()\n            pluginDir.deleteRecursively()\n            if (!pluginDir.mkdirs()) throw FileNotFoundException(\"Unable to create plugin directory\")\n            val pluginDirPath = pluginDir.absolutePath + '/'\n            do {\n                val path = cursor.getString(0)\n                val file = File(pluginDir, path)\n                check(file.absolutePath.startsWith(pluginDirPath))\n                cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->\n                    file.outputStream().use { outStream -> inStream.copyTo(outStream) }\n                }\n                Os.chmod(file.absolutePath, when (cursor.getType(1)) {\n                    Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)\n                    Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)\n                    else -> throw IllegalArgumentException(\"File mode should be of type int\")\n                })\n                if (path == options.id) initialized = true\n            } while (cursor.moveToNext())\n        }\n        if (!initialized) entryNotFound()\n        return File(pluginDir, options.id).absolutePath\n    }\n\n    fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {\n        is String -> value\n        is Int -> SagerNet.application.packageManager.getResourcesForApplication(applicationInfo).getString(value)\n        null -> null\n        else -> error(\"meta-data $key has invalid type ${value.javaClass}\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport java.util.*\n\n/**\n * Helper class for processing plugin options.\n *\n * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java\n */\nclass PluginOptions : HashMap<String, String?> {\n    var id = \"\"\n\n    constructor() : super()\n    constructor(initialCapacity: Int) : super(initialCapacity)\n    constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)\n\n    private constructor(options: String?, parseId: Boolean) : this() {\n        @Suppress(\"NAME_SHADOWING\")\n        var parseId = parseId\n        if (options.isNullOrEmpty()) return\n        check(options.all { !it.isISOControl() }) { \"No control characters allowed.\" }\n        val tokenizer = StringTokenizer(\"$options;\", \"\\\\=;\", true)\n        val current = StringBuilder()\n        var key: String? = null\n        while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {\n            \"\\\\\" -> current.append(tokenizer.nextToken())\n            \"=\" -> if (key == null) {\n                key = current.toString()\n                current.setLength(0)\n            } else current.append(nextToken)\n            \";\" -> {\n                if (key != null) {\n                    put(key, current.toString())\n                    key = null\n                } else if (current.isNotEmpty()) {\n                    if (parseId) id = current.toString() else put(current.toString(), null)\n                }\n                current.setLength(0)\n                parseId = false\n            }\n            else -> current.append(nextToken)\n        }\n    }\n\n    constructor(options: String?) : this(options, true)\n    constructor(id: String, options: String?) : this(options, false) {\n        this.id = id\n    }\n\n    /**\n     * Put but if value is null or default, the entry is deleted.\n     *\n     * @return Old value before put.\n     */\n    fun putWithDefault(key: String, value: String?, default: String? = null) =\n            if (value == null || value == default) remove(key) else put(key, value)\n\n    private fun append(result: StringBuilder, str: String) = str.indices.map { str[it] }.forEach {\n        when (it) {\n            '\\\\', '=', ';' -> {\n                result.append('\\\\') // intentionally no break\n                result.append(it)\n            }\n            else -> result.append(it)\n        }\n    }\n\n    fun toString(trimId: Boolean): String {\n        val result = StringBuilder()\n        if (!trimId) if (id.isEmpty()) return \"\" else append(result, id)\n        for ((key, value) in entries) {\n            if (result.isNotEmpty()) result.append(';')\n            append(result, key)\n            if (value != null) {\n                result.append('=')\n                append(result, value)\n            }\n        }\n        return result.toString()\n    }\n\n    override fun toString(): String = toString(true)\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id\n    }\n    override fun hashCode(): Int = Objects.hash(super.hashCode(), id)\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin\n\nimport android.content.pm.ComponentInfo\nimport android.content.pm.ResolveInfo\nimport android.graphics.drawable.Drawable\nimport android.os.Build\nimport com.github.shadowsocks.plugin.PluginManager.loadString\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.ktx.signaturesCompat\n\nabstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {\n    protected abstract val componentInfo: ComponentInfo\n\n    override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }\n    override val idAliases: Array<String> by lazy {\n        when (val value = componentInfo.metaData.get(PluginContract.METADATA_KEY_ID_ALIASES)) {\n            is String -> arrayOf(value)\n            is Int -> SagerNet.application.packageManager.getResourcesForApplication(componentInfo.applicationInfo)\n                .run {\n                    when (getResourceTypeName(value)) {\n                        \"string\" -> arrayOf(getString(value))\n                        else -> getStringArray(value)\n                    }\n                }\n            null -> emptyArray()\n            else -> error(\"unknown type for plugin meta-data idAliases\")\n        }\n    }\n    override val label: CharSequence get() = resolveInfo.loadLabel(SagerNet.application.packageManager)\n    override val icon: Drawable get() = resolveInfo.loadIcon(SagerNet.application.packageManager)\n    override val defaultConfig by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }\n    override val packageName: String get() = componentInfo.packageName\n    override val trusted by lazy {\n        SagerNet.application.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)\n    }\n    override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/Utils.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n@file:JvmName(\"Utils\")\n\npackage com.github.shadowsocks.plugin\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass Empty : Parcelable"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/plugin/fragment/AlertDialogFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.plugin.fragment\n\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.os.Parcelable\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatDialogFragment\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.setFragmentResult\nimport androidx.fragment.app.setFragmentResultListener\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\n\n/**\n * Based on: https://android.googlesource.com/platform/\npackages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java\n */\nabstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable?> :\n    AppCompatDialogFragment(), DialogInterface.OnClickListener {\n    companion object {\n        private const val KEY_RESULT = \"result\"\n        private const val KEY_ARG = \"arg\"\n        private const val KEY_RET = \"ret\"\n        private const val KEY_WHICH = \"which\"\n\n        fun <Ret : Parcelable> setResultListener(fragment: Fragment, requestKey: String,\n                                                 listener: (Int, Ret?) -> Unit) {\n            fragment.setFragmentResultListener(requestKey) { _, bundle ->\n                listener(bundle.getInt(KEY_WHICH, Activity.RESULT_CANCELED), bundle.getParcelable(KEY_RET))\n            }\n        }\n        inline fun <reified T : AlertDialogFragment<*, Ret>, Ret : Parcelable?> setResultListener(\n            fragment: Fragment, noinline listener: (Int, Ret?) -> Unit) =\n            setResultListener(fragment, T::class.java.name, listener)\n    }\n    protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)\n\n    private val resultKey get() = requireArguments().getString(KEY_RESULT)\n    protected val arg by lazy { requireArguments().getParcelable<Arg>(KEY_ARG)!! }\n    protected open fun ret(which: Int): Ret? = null\n\n    private fun args() = arguments ?: Bundle().also { arguments = it }\n    fun arg(arg: Arg) = args().putParcelable(KEY_ARG, arg)\n    fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =\n        MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()\n\n    override fun onClick(dialog: DialogInterface?, which: Int) {\n        setFragmentResult(resultKey ?: return, Bundle().apply {\n            putInt(KEY_WHICH, which)\n            putParcelable(KEY_RET, ret(which) ?: return@apply)\n        })\n    }\n\n    override fun onDismiss(dialog: DialogInterface) {\n        super.onDismiss(dialog)\n        onClick(null, Activity.RESULT_CANCELED)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/preference/PluginConfigurationDialogFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.preference\n\nimport android.view.View\nimport android.widget.EditText\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.os.bundleOf\nimport androidx.preference.EditTextPreferenceDialogFragmentCompat\nimport androidx.preference.PreferenceDialogFragmentCompat\nimport com.github.shadowsocks.plugin.PluginContract\nimport com.github.shadowsocks.plugin.PluginManager\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity\nimport io.nekohasekai.sagernet.ui.profile.TrojanGoSettingsActivity\n\nclass PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {\n    companion object {\n        private const val PLUGIN_ID_FRAGMENT_TAG =\n            \"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID\"\n    }\n\n    fun setArg(key: String, plugin: String) {\n        arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key,\n            PLUGIN_ID_FRAGMENT_TAG to plugin)\n    }\n\n    private lateinit var editText: EditText\n\n    override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {\n        super.onPrepareDialogBuilder(builder)\n        val intent = PluginManager.buildIntent(arguments?.getString(PLUGIN_ID_FRAGMENT_TAG)!!,\n            PluginContract.ACTION_HELP)\n        val activity = activity\n        if (activity is ShadowsocksSettingsActivity) {\n            if (intent.resolveActivity(app.packageManager) != null) builder.setNeutralButton(\"?\") { _, _ ->\n                activity.pluginHelp.launch(intent.putExtra(PluginContract.EXTRA_OPTIONS,\n                    editText.text.toString()))\n            }\n        } else {\n            activity as TrojanGoSettingsActivity\n            if (intent.resolveActivity(app.packageManager) != null) builder.setNeutralButton(\n                \"?\") { _, _ ->\n                activity.pluginHelp.launch(intent.putExtra(PluginContract.EXTRA_OPTIONS,\n                    editText.text.toString()))\n            }\n        }\n    }\n\n    override fun onBindDialogView(view: View) {\n        super.onBindDialogView(view)\n        editText = view.findViewById(android.R.id.edit)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/preference/PluginPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.preference\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport androidx.preference.ListPreference\nimport com.github.shadowsocks.plugin.PluginList\nimport com.github.shadowsocks.plugin.PluginManager\nimport io.nekohasekai.sagernet.R\n\nclass PluginPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(\n    context, attrs\n) {\n    companion object FallbackProvider : SummaryProvider<PluginPreference> {\n        override fun provideSummary(preference: PluginPreference) = preference.selectedEntry?.label\n            ?: preference.unknownValueSummary.format(preference.value)\n    }\n\n    lateinit var plugins: PluginList\n    val selectedEntry get() = plugins.lookup[value]\n    private val entryIcon: Drawable? get() = selectedEntry?.icon\n    private val unknownValueSummary = context.getString(R.string.plugin_unknown)\n\n    private var listener: OnPreferenceChangeListener? = null\n    override fun getOnPreferenceChangeListener(): OnPreferenceChangeListener? = listener\n    override fun setOnPreferenceChangeListener(listener: OnPreferenceChangeListener?) {\n        this.listener = listener\n    }\n\n    init {\n        super.setOnPreferenceChangeListener { preference, newValue ->\n            val listener = listener\n            if (listener == null || listener.onPreferenceChange(preference, newValue)) {\n                value = newValue.toString()\n                icon = entryIcon\n                true\n            } else false\n        }\n    }\n\n    fun init(skipInternal: Boolean = false) {\n        plugins = PluginManager.fetchPlugins(skipInternal)\n        entryValues = plugins.lookup.map { it.key }.toTypedArray()\n        icon = entryIcon\n        summaryProvider = FallbackProvider\n    }\n\n    override fun onSetInitialValue(defaultValue: Any?) {\n        super.onSetInitialValue(defaultValue)\n        init()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/shadowsocks/preference/PluginPreferenceDialogFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage com.github.shadowsocks.preference\n\nimport android.app.Dialog\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.graphics.Typeface\nimport android.net.Uri\nimport android.os.Bundle\nimport android.provider.Settings\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.core.os.bundleOf\nimport androidx.core.view.isGone\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.setFragmentResult\nimport androidx.preference.PreferenceDialogFragmentCompat\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.github.shadowsocks.plugin.Plugin\nimport com.google.android.material.bottomsheet.BottomSheetDialog\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.databinding.LayoutIconListItem2Binding\n\nclass PluginPreferenceDialogFragment : PreferenceDialogFragmentCompat() {\n    companion object {\n        const val KEY_SELECTED_ID = \"id\"\n    }\n\n    private inner class IconListViewHolder(val dialog: BottomSheetDialog, binding: LayoutIconListItem2Binding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener, View.OnLongClickListener {\n        private lateinit var plugin: Plugin\n        private val text1 = binding.text1\n        private val text2 = binding.text2\n        private val icon = binding.icon\n        private val unlock = binding.unlock.apply {\n            TooltipCompat.setTooltipText(this, getText(R.string.plugin_auto_connect_unlock_only))\n        }\n\n        init {\n            binding.root.setOnClickListener(this)\n            binding.root.setOnLongClickListener(this)\n        }\n\n        fun bind(plugin: Plugin, selected: Boolean = false) {\n            this.plugin = plugin\n            val label = plugin.label\n            text1.text = label\n            text2.text = plugin.id\n            val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL\n            text1.setTypeface(null, typeface)\n            text2.setTypeface(null, typeface)\n            text2.isVisible = plugin.id.isNotEmpty() && label != plugin.id\n            icon.setImageDrawable(plugin.icon)\n            unlock.isGone = plugin.directBootAware || !DataStore.persistAcrossReboot\n        }\n\n        override fun onClick(v: View?) {\n            clicked = plugin\n            dialog.dismiss()\n        }\n\n        override fun onLongClick(v: View?) = try {\n            startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.Builder()\n                .scheme(\"package\").opaquePart(plugin.packageName).build()))\n            true\n        } catch (_: ActivityNotFoundException) {\n            false\n        }\n    }\n\n    private inner class IconListAdapter(private val dialog: BottomSheetDialog) : RecyclerView.Adapter<IconListViewHolder>() {\n        override fun getItemCount(): Int = preference.plugins.size\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =\n            IconListViewHolder(dialog, LayoutIconListItem2Binding.inflate(layoutInflater, parent, false))\n\n        override fun onBindViewHolder(holder: IconListViewHolder, position: Int) {\n            if (selected < 0) holder.bind(preference.plugins[position]) else when (position) {\n                0 -> holder.bind(preference.selectedEntry!!, true)\n                in selected + 1..Int.MAX_VALUE -> holder.bind(preference.plugins[position])\n                else -> holder.bind(preference.plugins[position - 1])\n            }\n        }\n    }\n\n    fun setArg(key: String) {\n        arguments = bundleOf(ARG_KEY to key)\n    }\n\n    private val preference by lazy { getPreference() as PluginPreference }\n    private val selected by lazy { preference.plugins.indexOf(preference.selectedEntry) }\n    private var clicked: Plugin? = null\n\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        val activity = requireActivity()\n        val dialog = BottomSheetDialog(activity, theme)\n        val recycler = RecyclerView(activity)\n        val padding = resources.getDimensionPixelOffset(R.dimen.bottom_sheet_padding)\n        recycler.setPadding(0, padding, 0, padding)\n        recycler.setHasFixedSize(true)\n        recycler.layoutManager = LinearLayoutManager(activity)\n        recycler.itemAnimator = DefaultItemAnimator()\n        recycler.adapter = IconListAdapter(dialog)\n        recycler.layoutParams =\n            ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)\n        dialog.setContentView(recycler)\n        return dialog\n    }\n\n    override fun onDialogClosed(positiveResult: Boolean) {\n        val clicked = clicked\n        if (clicked != null && clicked != preference.selectedEntry) {\n            setFragmentResult(javaClass.name, bundleOf(KEY_SELECTED_ID to clicked.id))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet\n\nimport android.content.BroadcastReceiver\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport io.nekohasekai.sagernet.bg.SubscriptionUpdater\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\n\nclass BootReceiver : BroadcastReceiver() {\n    companion object {\n        private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }\n        var enabled: Boolean\n            get() = app.packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED\n            set(value) = app.packageManager.setComponentEnabledSetting(\n                componentName, if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED\n                else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP\n            )\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        runOnDefaultDispatcher {\n            SubscriptionUpdater.reconfigureUpdater()\n        }\n\n        if (!DataStore.persistAcrossReboot) {   // sanity check\n            enabled = false\n            return\n        }\n\n        val doStart = when (intent.action) {\n            Intent.ACTION_LOCKED_BOOT_COMPLETED -> DataStore.directBootAware\n            else -> Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked\n        } && DataStore.currentProfile > 0\n\n        if (doStart) SagerNet.startService()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/Constants.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet\n\nconst val CONNECTION_TEST_URL = \"https://api.v2fly.org/checkConnection.svgz\"\n\nobject Key {\n\n    const val DB_PUBLIC = \"configuration.db\"\n    const val DB_PROFILE = \"sager_net.db\"\n    const val DISABLE_AEAD = \"V2RAY_VMESS_AEAD_DISABLED\"\n\n    const val PERSIST_ACROSS_REBOOT = \"isAutoConnect\"\n    const val DIRECT_BOOT_AWARE = \"directBootAware\"\n\n    const val APP_THEME = \"appTheme\"\n    const val NIGHT_THEME = \"nightTheme\"\n    const val SERVICE_MODE = \"serviceMode\"\n    const val MODE_VPN = \"vpn\"\n    const val MODE_PROXY = \"proxy\"\n\n    const val REMOTE_DNS = \"remoteDns\"\n    const val DIRECT_DNS = \"directDns\"\n    const val ENABLE_DNS_ROUTING = \"enableDnsRouting\"\n    const val ENABLE_FAKEDNS = \"enableFakeDns\"\n    const val DNS_HOSTS = \"dnsHosts\"\n\n    const val IPV6_MODE = \"ipv6Mode\"\n\n    const val PROXY_APPS = \"proxyApps\"\n    const val BYPASS_MODE = \"bypassMode\"\n    const val INDIVIDUAL = \"individual\"\n    const val METERED_NETWORK = \"meteredNetwork\"\n\n    const val DOMAIN_STRATEGY = \"domainStrategy\"\n    const val TRAFFIC_SNIFFING = \"trafficSniffing\"\n    const val DESTINATION_OVERRIDE = \"destinationOverride\"\n    const val RESOLVE_DESTINATION = \"resolveDestination\"\n\n    const val BYPASS_LAN = \"bypassLan\"\n    const val BYPASS_LAN_IN_CORE_ONLY = \"bypassLanInCoreOnly\"\n\n    const val SOCKS_PORT = \"socksPort\"\n    const val ALLOW_ACCESS = \"allowAccess\"\n    const val SPEED_INTERVAL = \"speedInterval\"\n    const val SHOW_DIRECT_SPEED = \"showDirectSpeed\"\n    const val LOCAL_DNS_PORT = \"portLocalDns\"\n\n    const val REQUIRE_HTTP = \"requireHttp\"\n    const val APPEND_HTTP_PROXY = \"appendHttpProxy\"\n    const val HTTP_PORT = \"httpPort\"\n\n    const val REQUIRE_TRANSPROXY = \"requireTransproxy\"\n    const val TRANSPROXY_MODE = \"transproxyMode\"\n    const val TRANSPROXY_PORT = \"transproxyPort\"\n\n    const val CONNECTION_TEST_URL = \"connectionTestURL\"\n\n    const val ENABLE_MUX = \"enableMux\"\n    const val ENABLE_MUX_FOR_ALL = \"enableMuxForAll\"\n    const val MUX_CONCURRENCY = \"muxConcurrency\"\n    const val SHOW_STOP_BUTTON = \"showStopButton\"\n    const val SECURITY_ADVISORY = \"securityAdvisory\"\n    const val TCP_KEEP_ALIVE_INTERVAL = \"tcpKeepAliveInterval\"\n    const val RULES_PROVIDER = \"rulesProvider\"\n    const val ENABLE_LOG = \"enableLog\"\n\n    const val ALWAYS_SHOW_ADDRESS = \"alwaysShowAddress\"\n\n    const val PROVIDER_TROJAN = \"providerTrojan\"\n    const val PROVIDER_SS_AEAD = \"providerShadowsocksAEAD\"\n    const val PROVIDER_SS_STREAM = \"providerShadowsocksStream\"\n\n    const val UTLS_FINGERPRINT = \"utlsFingerprint\"\n    const val TUN_IMPLEMENTATION = \"tunImplementation\"\n    const val ENABLE_PCAP = \"enablePcap\"\n\n    const val APP_TRAFFIC_STATISTICS = \"appTrafficStatistics\"\n    const val PROFILE_TRAFFIC_STATISTICS = \"profileTrafficStatistics\"\n\n    const val PROFILE_DIRTY = \"profileDirty\"\n    const val PROFILE_ID = \"profileId\"\n    const val PROFILE_NAME = \"profileName\"\n    const val PROFILE_GROUP = \"profileGroup\"\n    const val PROFILE_STARTED = \"profileStarted\"\n    const val PROFILE_CURRENT = \"profileCurrent\"\n\n    const val SERVER_ADDRESS = \"serverAddress\"\n    const val SERVER_PORT = \"serverPort\"\n    const val SERVER_USERNAME = \"serverUsername\"\n    const val SERVER_PASSWORD = \"serverPassword\"\n    const val SERVER_METHOD = \"serverMethod\"\n    const val SERVER_PLUGIN = \"serverPlugin\"\n    const val SERVER_PLUGIN_CONFIGURE = \"serverPluginConfigure\"\n    const val SERVER_PASSWORD1 = \"serverPassword1\"\n\n    const val SERVER_PROTOCOL = \"serverProtocol\"\n    const val SERVER_PROTOCOL_PARAM = \"serverProtocolParam\"\n    const val SERVER_OBFS = \"serverObfs\"\n    const val SERVER_OBFS_PARAM = \"serverObfsParam\"\n\n    const val SERVER_USER_ID = \"serverUserId\"\n    const val SERVER_ALTER_ID = \"serverAlterId\"\n    const val SERVER_SECURITY = \"serverSecurity\"\n    const val SERVER_NETWORK = \"serverNetwork\"\n    const val SERVER_HEADER = \"serverHeader\"\n    const val SERVER_HOST = \"serverHost\"\n    const val SERVER_PATH = \"serverPath\"\n    const val SERVER_SNI = \"serverSNI\"\n    const val SERVER_TLS = \"serverTLS\"\n    const val SERVER_ENCRYPTION = \"serverEncryption\"\n    const val SERVER_ALPN = \"serverALPN\"\n    const val SERVER_CERTIFICATES = \"serverCertificates\"\n    const val SERVER_FLOW = \"serverFlow\"\n    const val SERVER_QUIC_SECURITY = \"serverQuicSecurity\"\n    const val SERVER_WS_BROWSER_FORWARDING = \"serverWsBrowserForwarding\"\n    const val SERVER_CONFIG = \"serverConfig\"\n\n    const val SERVER_SECURITY_CATEGORY = \"serverSecurityCategory\"\n    const val SERVER_WS_CATEGORY = \"serverWsCategory\"\n    const val SERVER_SS_CATEGORY = \"serverSsCategory\"\n    const val SERVER_HEADERS = \"serverHeaders\"\n    const val SERVER_MULTI_MODE = \"serverMultiMode\"\n    const val SERVER_ALLOW_INSECURE = \"serverAllowInsecure\"\n\n    const val SERVER_AUTH_TYPE = \"serverAuthType\"\n    const val SERVER_UPLOAD_SPEED = \"serverUploadSpeed\"\n    const val SERVER_DOWNLOAD_SPEED = \"serverDownloadSpeed\"\n    const val SERVER_STREAM_RECEIVE_WINDOW = \"serverStreamReceiveWindow\"\n    const val SERVER_CONNECTION_RECEIVE_WINDOW = \"serverConnectionReceiveWindow\"\n    const val SERVER_DISABLE_MTU_DISCOVERY = \"serverDisableMtuDiscovery\"\n\n    const val SERVER_VMESS_EXPERIMENTS_CATEGORY = \"serverVMessExperimentsCategory\"\n    const val SERVER_VMESS_EXPERIMENTAL_AUTHENTICATED_LENGTH = \"serverVMessExperimentalAuthenticatedLength\"\n    const val SERVER_VMESS_EXPERIMENTAL_NO_TERMINATION_SIGNAL = \"serverVMessExperimentalNoTerminationSignal\"\n\n    const val SERVER_PRIVATE_KEY = \"serverPrivateKey\"\n    const val SERVER_LOCAL_ADDRESS = \"serverLocalAddress\"\n\n    const val BALANCER_TYPE = \"balancerType\"\n    const val BALANCER_GROUP = \"balancerGroup\"\n    const val BALANCER_STRATEGY = \"balancerStrategy\"\n\n    const val ROUTE_NAME = \"routeName\"\n    const val ROUTE_DOMAIN = \"routeDomain\"\n    const val ROUTE_IP = \"routeIP\"\n    const val ROUTE_PORT = \"routePort\"\n    const val ROUTE_SOURCE_PORT = \"routeSourcePort\"\n    const val ROUTE_NETWORK = \"routeNetwork\"\n    const val ROUTE_SOURCE = \"routeSource\"\n    const val ROUTE_PROTOCOL = \"routeProtocol\"\n    const val ROUTE_ATTRS = \"routeAttrs\"\n    const val ROUTE_OUTBOUND = \"routeOutbound\"\n    const val ROUTE_OUTBOUND_RULE = \"routeOutboundRule\"\n    const val ROUTE_REVERSE = \"routeReverse\"\n    const val ROUTE_REDIRECT = \"routeRedirect\"\n    const val ROUTE_PACKAGES = \"routePackages\"\n    const val ROUTE_FOREGROUND_STATUS = \"routeForegroundStatus\"\n\n    const val GROUP_NAME = \"groupName\"\n    const val GROUP_TYPE = \"groupType\"\n    const val GROUP_ORDER = \"groupOrder\"\n\n    const val GROUP_SUBSCRIPTION = \"groupSubscription\"\n    const val SUBSCRIPTION_TYPE = \"subscriptionType\"\n    const val SUBSCRIPTION_LINK = \"subscriptionLink\"\n    const val SUBSCRIPTION_TOKEN = \"subscriptionToken\"\n    const val SUBSCRIPTION_FORCE_RESOLVE = \"subscriptionForceResolve\"\n    const val SUBSCRIPTION_DEDUPLICATION = \"subscriptionDeduplication\"\n    const val SUBSCRIPTION_FORCE_VMESS_AEAD = \"subscriptionForceVMessAEAD\"\n    const val SUBSCRIPTION_UPDATE = \"subscriptionUpdate\"\n    const val SUBSCRIPTION_UPDATE_WHEN_CONNECTED_ONLY = \"subscriptionUpdateWhenConnectedOnly\"\n    const val SUBSCRIPTION_USER_AGENT = \"subscriptionUserAgent\"\n    const val SUBSCRIPTION_AUTO_UPDATE = \"subscriptionAutoUpdate\"\n    const val SUBSCRIPTION_AUTO_UPDATE_DELAY = \"subscriptionAutoUpdateDelay\"\n\n}\n\nobject TunImplementation {\n    const val GVISOR = 0\n    const val LWIP = 1\n}\n\nobject TrojanProvider {\n    const val V2RAY = 0\n    const val TROJAN = 1\n    const val TROJAN_GO = 2\n}\n\nobject ShadowsocksProvider {\n    const val V2RAY = 0\n    const val SHADOWSOCKS_RUST = 1\n    const val CLASH = 2\n    const val SHADOWSOCKS_LIBEV = 3\n}\n\nobject ShadowsocksStreamProvider {\n    const val SHADOWSOCKS_RUST = 0\n    const val CLASH = 1\n    const val SHADOWSOCKS_LIBEV = 2\n}\n\nobject IPv6Mode {\n    const val DISABLE = 0\n    const val ENABLE = 1\n    const val PREFER = 2\n    const val ONLY = 3\n}\n\nobject PacketStrategy {\n    const val DIRECT = 0\n    const val DROP = 1\n    const val REPLY = 2\n}\n\nobject GroupType {\n    const val BASIC = 0\n    const val SUBSCRIPTION = 1\n}\n\nobject SubscriptionType {\n    const val RAW = 0\n    const val OOCv1 = 1\n    const val SIP008 = 2\n}\n\nobject ExtraType {\n    const val NONE = 0\n    const val OOCv1 = 1\n    const val SIP008 = 2\n}\n\nobject GroupOrder {\n    const val ORIGIN = 0\n    const val BY_NAME = 1\n    const val BY_DELAY = 2\n}\n\nobject AppStatus {\n    const val FOREGROUND = \"foreground\"\n    const val BACKGROUND = \"background\"\n}\n\nobject Action {\n    const val SERVICE = \"io.nekohasekai.sagernet.SERVICE\"\n    const val CLOSE = \"io.nekohasekai.sagernet.CLOSE\"\n    const val RELOAD = \"io.nekohasekai.sagernet.RELOAD\"\n    const val ABORT = \"io.nekohasekai.sagernet.ABORT\"\n\n    const val EXTRA_PROFILE_ID = \"io.nekohasekai.sagernet.EXTRA_PROFILE_ID\"\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/QuickToggleShortcut.kt",
    "content": "/*******************************************************************************\n *                                                                             *\n *  Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com>                          *\n *  Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                             *\n *******************************************************************************/\n\npackage io.nekohasekai.sagernet\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.pm.ShortcutManager\nimport android.os.Build\nimport android.os.Bundle\nimport androidx.core.content.getSystemService\nimport androidx.core.content.pm.ShortcutInfoCompat\nimport androidx.core.content.pm.ShortcutManagerCompat\nimport androidx.core.graphics.drawable.IconCompat\nimport io.nekohasekai.sagernet.aidl.ISagerNetService\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.bg.SagerConnection\n\n@Suppress(\"DEPRECATION\")\nclass QuickToggleShortcut : Activity(), SagerConnection.Callback {\n    private val connection = SagerConnection()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {\n            setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,\n                ShortcutInfoCompat.Builder(this, \"toggle\")\n                    .setIntent(Intent(this,\n                        QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))\n                    .setIcon(IconCompat.createWithResource(this,\n                        R.drawable.ic_qu_shadowsocks_launcher))\n                    .setShortLabel(getString(R.string.quick_toggle))\n                    .build()))\n            finish()\n        } else {\n            connection.connect(this, this)\n            if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed(\n                \"toggle\")\n        }\n    }\n\n    override fun onServiceDisconnected() {\n        super.onServiceDisconnected()\n    }\n\n    override fun onServiceConnected(service: ISagerNetService) {\n        val state = BaseService.State.values()[service.state]\n        when {\n            state.canStop -> SagerNet.stopService()\n            state == BaseService.State.Stopped -> SagerNet.startService()\n        }\n        finish()\n    }\n\n    override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {}\n\n    override fun onDestroy() {\n        connection.disconnect(this)\n        super.onDestroy()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet\n\nimport android.annotation.SuppressLint\nimport android.app.*\nimport android.app.admin.DevicePolicyManager\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageManager\nimport android.content.res.Configuration\nimport android.net.ConnectivityManager\nimport android.os.Build\nimport android.os.StrictMode\nimport android.os.UserManager\nimport androidx.annotation.RequiresApi\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.getSystemService\nimport go.Seq\nimport io.nekohasekai.sagernet.bg.SagerConnection\nimport io.nekohasekai.sagernet.bg.proto.UidDumper\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.checkMT\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.ui.MainActivity\nimport io.nekohasekai.sagernet.utils.CrashHandler\nimport io.nekohasekai.sagernet.utils.DeviceStorageApp\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport io.nekohasekai.sagernet.utils.Theme\nimport kotlinx.coroutines.DEBUG_PROPERTY_NAME\nimport kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON\nimport libcore.Libcore\nimport org.conscrypt.Conscrypt\nimport java.security.Security\nimport androidx.work.Configuration as WorkConfiguration\n\nclass SagerNet : Application(),\n    WorkConfiguration.Provider {\n\n    override fun attachBaseContext(base: Context) {\n        super.attachBaseContext(base)\n\n        application = this\n    }\n\n    val externalAssets by lazy { getExternalFilesDir(null) ?: filesDir }\n\n    override fun onCreate() {\n        super.onCreate()\n\n        System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)\n        Thread.setDefaultUncaughtExceptionHandler(CrashHandler)\n        DataStore.init()\n        updateNotificationChannels()\n        Seq.setContext(this)\n\n        externalAssets.mkdirs()\n        Libcore.initializeV2Ray(\n            filesDir.absolutePath + \"/\", externalAssets.absolutePath + \"/\", \"v2ray/\"\n        ) {\n            DataStore.rulesProvider == 0\n        }\n        //Libcore.setenv(\"v2ray.conf.geoloader\", \"memconservative\")\n        Libcore.setUidDumper(UidDumper)\n\n        runOnDefaultDispatcher {\n            PackageCache.register()\n            checkMT()\n        }\n\n        Theme.apply(this)\n        Theme.applyNightTheme()\n\n        Security.insertProviderAt(Conscrypt.newProvider(), 1)\n\n        if (BuildConfig.DEBUG) StrictMode.setVmPolicy(\n            StrictMode.VmPolicy.Builder()\n                .detectLeakedSqlLiteObjects()\n                .detectLeakedClosableObjects()\n                .detectLeakedRegistrationObjects()\n                .penaltyLog()\n                .build()\n        )\n    }\n\n    fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(\n        packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES\n        else @Suppress(\"DEPRECATION\") PackageManager.GET_SIGNATURES\n    )!!\n\n    override fun onConfigurationChanged(newConfig: Configuration) {\n        super.onConfigurationChanged(newConfig)\n        updateNotificationChannels()\n    }\n\n    override fun getWorkManagerConfiguration(): WorkConfiguration {\n        return WorkConfiguration.Builder()\n            .setDefaultProcessName(\"${BuildConfig.APPLICATION_ID}:bg\")\n            .build()\n    }\n\n    @SuppressLint(\"InlinedApi\")\n    companion object {\n\n        @Volatile\n        var started = false\n\n        lateinit var application: SagerNet\n\n        val isTv by lazy {\n            uiMode.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION\n        }\n\n        val deviceStorage by lazy {\n            if (Build.VERSION.SDK_INT < 24) application else DeviceStorageApp(application)\n        }\n\n        val configureIntent: (Context) -> PendingIntent by lazy {\n            {\n                PendingIntent.getActivity(\n                    it,\n                    0,\n                    Intent(\n                        application, MainActivity::class.java\n                    ).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0\n                )\n            }\n        }\n        val activity by lazy { application.getSystemService<ActivityManager>()!! }\n        val clipboard by lazy { application.getSystemService<ClipboardManager>()!! }\n        val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }\n        val notification by lazy { application.getSystemService<NotificationManager>()!! }\n        val user by lazy { application.getSystemService<UserManager>()!! }\n        val uiMode by lazy { application.getSystemService<UiModeManager>()!! }\n        val packageInfo: PackageInfo by lazy { application.getPackageInfo(application.packageName) }\n        val directBootSupported by lazy {\n            Build.VERSION.SDK_INT >= 24 && try {\n                app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER\n            } catch (_: RuntimeException) {\n                false\n            }\n        }\n\n        val currentProfile get() = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)\n\n        fun getClipboardText(): String {\n            return clipboard.primaryClip?.takeIf { it.itemCount > 0 }\n                ?.getItemAt(0)?.text?.toString() ?: \"\"\n        }\n\n        fun trySetPrimaryClip(clip: String) = try {\n            clipboard.setPrimaryClip(ClipData.newPlainText(null, clip))\n            true\n        } catch (e: RuntimeException) {\n            Logs.w(e)\n            false\n        }\n\n        fun updateNotificationChannels() {\n            if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {\n                notification.createNotificationChannels(\n                    listOf(\n                        NotificationChannel(\n                            \"service-vpn\",\n                            application.getText(R.string.service_vpn),\n                            if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN\n                            else NotificationManager.IMPORTANCE_LOW\n                        ),   // #1355\n                        NotificationChannel(\n                            \"service-proxy\",\n                            application.getText(R.string.service_proxy),\n                            NotificationManager.IMPORTANCE_LOW\n                        ), NotificationChannel(\n                            \"service-subscription\",\n                            application.getText(R.string.service_subscription),\n                            NotificationManager.IMPORTANCE_DEFAULT\n                        )\n                    )\n                )\n            }\n        }\n\n        fun startService() = ContextCompat.startForegroundService(\n            application, Intent(application, SagerConnection.serviceClass)\n        )\n\n        fun reloadService() =\n            application.sendBroadcast(Intent(Action.RELOAD).setPackage(application.packageName))\n\n        fun stopService() =\n            application.sendBroadcast(Intent(Action.CLOSE).setPackage(application.packageName))\n\n    }\n\n    override fun onLowMemory() {\n        super.onLowMemory()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/aidl/AppStats.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.aidl\n\nimport android.os.Parcelable\nimport io.nekohasekai.sagernet.database.StatsEntity\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class AppStats(\n    var packageName: String,\n    var uid: Int,\n    var tcpConnections: Int,\n    var udpConnections: Int,\n    var tcpConnectionsTotal: Int,\n    var udpConnectionsTotal: Int,\n    var uplink: Long,\n    var downlink: Long,\n    var uplinkTotal: Long,\n    var downlinkTotal: Long,\n    var deactivateAt: Int\n) : Parcelable {\n\n   operator fun plusAssign(stats: StatsEntity) {\n       tcpConnectionsTotal += stats.tcpConnections\n       udpConnectionsTotal += stats.udpConnections\n       uplinkTotal += stats.uplink\n       downlinkTotal += stats.downlink\n   }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/aidl/AppStatsList.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.aidl\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass AppStatsList(\n    var data: List<AppStats>\n) : Parcelable"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/aidl/TrafficStats.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.aidl\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class TrafficStats(\n    // Bytes per second\n    var txRateProxy: Long = 0L,\n    var rxRateProxy: Long = 0L,\n    var txRateDirect: Long = 0L,\n    var rxRateDirect: Long = 0L,\n\n    // Bytes for the current session\n    // Outbound \"bypass\" usage is not counted\n    var txTotal: Long = 0L,\n    var rxTotal: Long = 0L,\n) : Parcelable {\n    operator fun plus(other: TrafficStats) = TrafficStats(\n        txRateProxy + other.txRateProxy, rxRateProxy + other.rxRateProxy,\n        txRateDirect + other.txRateDirect, rxRateDirect + other.rxRateDirect,\n        txTotal + other.txTotal, rxTotal + other.rxTotal)\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/AbstractInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport java.io.Closeable\n\ninterface AbstractInstance : Closeable {\n\n    fun launch()\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.app.Service\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.os.Build\nimport android.os.IBinder\nimport android.os.RemoteCallbackList\nimport android.os.RemoteException\nimport cn.hutool.json.JSONException\nimport io.nekohasekai.sagernet.Action\nimport io.nekohasekai.sagernet.BootReceiver\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.aidl.AppStatsList\nimport io.nekohasekai.sagernet.aidl.ISagerNetService\nimport io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback\nimport io.nekohasekai.sagernet.aidl.TrafficStats\nimport io.nekohasekai.sagernet.bg.proto.ProxyInstance\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.fmt.TAG_SOCKS\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.plugin.PluginManager\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport kotlinx.coroutines.*\nimport libcore.AppStats\nimport libcore.Libcore\nimport libcore.TrafficListener\nimport java.net.UnknownHostException\nimport com.github.shadowsocks.plugin.PluginManager as ShadowsocksPluginPluginManager\nimport io.nekohasekai.sagernet.aidl.AppStats as AidlAppStats\n\nclass BaseService {\n\n    enum class State(val canStop: Boolean = false) {\n        /**\n         * Idle state is only used by UI and will never be returned by BaseService.\n         */\n        Idle,\n        Connecting(true),\n        Connected(true),\n        Stopping,\n        Stopped,\n    }\n\n    interface ExpectedException\n    class ExpectedExceptionWrapper(e: Exception) : Exception(e.localizedMessage, e),\n        ExpectedException\n\n    class Data internal constructor(private val service: Interface) {\n        var state = State.Stopped\n        var proxy: ProxyInstance? = null\n        var notification: ServiceNotification? = null\n\n        val closeReceiver = broadcastReceiver { _, intent ->\n            when (intent.action) {\n                Intent.ACTION_SHUTDOWN -> service.persistStats()\n                Action.RELOAD -> service.forceLoad()\n                else -> service.stopRunner(keepState = false)\n            }\n        }\n        var closeReceiverRegistered = false\n\n        val binder = Binder(this)\n        var connectingJob: Job? = null\n\n        fun changeState(s: State, msg: String? = null) {\n            if (state == s && msg == null) return\n            binder.stateChanged(s, msg)\n            state = s\n        }\n    }\n\n    class Binder(private var data: Data? = null) : ISagerNetService.Stub(),\n        CoroutineScope,\n        AutoCloseable,\n        TrafficListener {\n        private val callbacks = object : RemoteCallbackList<ISagerNetServiceCallback>() {\n            override fun onCallbackDied(callback: ISagerNetServiceCallback?, cookie: Any?) {\n                super.onCallbackDied(callback, cookie)\n                stopListeningForBandwidth(callback ?: return)\n                stopListeningForStats(callback)\n            }\n        }\n        private val bandwidthListeners = mutableMapOf<IBinder, Long>()  // the binder is the real identifier\n        private val statsListeners = mutableMapOf<IBinder, Long>()  // the binder is the real identifier\n        override val coroutineContext = Dispatchers.Main.immediate + Job()\n        private var looper: Job? = null\n        private var statsLooper: Job? = null\n\n        override fun getState(): Int = (data?.state ?: State.Idle).ordinal\n        override fun getProfileName(): String = data?.proxy?.profile?.displayName() ?: \"Idle\"\n\n        override fun registerCallback(cb: ISagerNetServiceCallback) {\n            callbacks.register(cb)\n        }\n\n        fun broadcast(work: (ISagerNetServiceCallback) -> Unit) {\n            val count = callbacks.beginBroadcast()\n            try {\n                repeat(count) {\n                    try {\n                        work(callbacks.getBroadcastItem(it))\n                    } catch (_: RemoteException) {\n                    } catch (e: Exception) {\n                    }\n                }\n            } finally {\n                callbacks.finishBroadcast()\n            }\n        }\n\n        private suspend fun loop() {\n            var lastQueryTime = 0L\n            val showDirectSpeed = DataStore.showDirectSpeed\n            while (true) {\n                val delayMs = bandwidthListeners.values.minOrNull()\n                delay(delayMs ?: return)\n                if (delayMs == 0L) return\n                val queryTime = System.currentTimeMillis()\n                val sinceLastQueryInSeconds = (queryTime - lastQueryTime).toDouble() / 1000L\n                val proxy = data?.proxy ?: continue\n                lastQueryTime = queryTime\n                val (statsOut, outs) = proxy.outboundStats()\n                val stats = TrafficStats(\n                    (proxy.uplinkProxy / sinceLastQueryInSeconds).toLong(),\n                    (proxy.downlinkProxy / sinceLastQueryInSeconds).toLong(),\n                    if (showDirectSpeed) (proxy.uplinkDirect() / sinceLastQueryInSeconds).toLong() else 0L,\n                    if (showDirectSpeed) (proxy.downlinkDirect() / sinceLastQueryInSeconds).toLong() else 0L,\n                    statsOut.uplinkTotal,\n                    statsOut.downlinkTotal\n                )\n                if (data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {\n                    broadcast { item ->\n                        if (bandwidthListeners.contains(item.asBinder())) {\n                            item.trafficUpdated(proxy.profile.id, stats, true)\n                            outs.forEach { (profileId, stats) ->\n                                item.trafficUpdated(\n                                    profileId, TrafficStats(\n                                        txRateDirect = stats.uplinkTotal,\n                                        rxTotal = stats.downlinkTotal\n                                    ), false\n                                )\n                            }\n                        }\n                    }\n                }\n\n            }\n\n        }\n\n        val appStats = ArrayList<AppStats>()\n        override fun updateStats(t: AppStats) {\n            appStats.add(t)\n        }\n\n        private suspend fun loopStats() {\n            var lastQueryTime = 0L\n            val tun = (data?.proxy?.service as? VpnService)?.getTun() ?: return\n            if (!tun.trafficStatsEnabled) return\n\n            while (true) {\n                val delayMs = statsListeners.values.minOrNull()\n                if (delayMs == 0L) return\n                val queryTime = System.currentTimeMillis()\n                val sinceLastQueryInSeconds = ((queryTime - lastQueryTime).toDouble() / 1000).toLong()\n                lastQueryTime = queryTime\n\n                appStats.clear()\n                tun.readAppTraffics(this)\n\n                val statsList = AppStatsList(appStats.map {\n                    val uid = if (it.uid >= 10000) it.uid else 1000\n                    val packageName = if (uid != 1000) {\n                        PackageCache.uidMap[it.uid]?.iterator()?.next() ?: \"android\"\n                    } else {\n                        \"android\"\n                    }\n                    AidlAppStats(\n                        packageName,\n                        uid, it.tcpConn, it.udpConn, it.tcpConnTotal, it.udpConnTotal,\n                        it.uplink / sinceLastQueryInSeconds,\n                        it.downlink / sinceLastQueryInSeconds,\n                        it.uplinkTotal,\n                        it.downlinkTotal,\n                        it.deactivateAt\n                    )\n                })\n                if (data?.state == State.Connected && statsListeners.isNotEmpty()) {\n                    broadcast { item ->\n                        if (statsListeners.contains(item.asBinder())) {\n                            item.statsUpdated(statsList)\n                        }\n                    }\n                }\n                delay(delayMs ?: return)\n            }\n\n        }\n\n        override fun startListeningForBandwidth(\n            cb: ISagerNetServiceCallback,\n            timeout: Long,\n        ) {\n            launch {\n                if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(\n                        cb.asBinder(), timeout\n                    ) == null)\n                ) {\n                    check(looper == null)\n                    looper = launch { loop() }\n                }\n                if (data?.state != State.Connected) return@launch\n                val data = data\n                data?.proxy ?: return@launch\n                val sum = TrafficStats()\n                cb.trafficUpdated(0, sum, true)\n            }\n        }\n\n        override fun stopListeningForBandwidth(cb: ISagerNetServiceCallback) {\n            launch {\n                if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {\n                    looper!!.cancel()\n                    looper = null\n                }\n            }\n        }\n\n        override fun unregisterCallback(cb: ISagerNetServiceCallback) {\n            stopListeningForBandwidth(cb)   // saves an RPC, and safer\n            stopListeningForStats(cb)\n            callbacks.unregister(cb)\n        }\n\n        override fun protect(fd: Int) {\n            (data?.proxy?.service as VpnService?)?.protect(fd)\n        }\n\n        override fun urlTest(): Int {\n            if (data?.proxy?.v2rayPoint == null) {\n                error(\"core not started\")\n            }\n            try {\n                return Libcore.urlTestV2ray(\n                    data!!.proxy!!.v2rayPoint, TAG_SOCKS, DataStore.connectionTestURL, 5000\n                )\n            } catch (e: Exception) {\n                var msg = e.readableMessage\n                if (msg.lowercase().contains(\"timeout\")) {\n                    msg = app.getString(R.string.connection_test_timeout)\n                } else if (msg.lowercase().contains(\"refused\")) {\n                    msg = app.getString(R.string.connection_test_refused)\n                }\n                error(msg)\n            }\n        }\n\n        override fun startListeningForStats(cb: ISagerNetServiceCallback, timeout: Long) {\n            launch {\n                if (statsListeners.isEmpty() and (statsListeners.put(\n                        cb.asBinder(), timeout\n                    ) == null)\n                ) {\n                    check(statsLooper == null)\n                    statsLooper = launch { loopStats() }\n                }\n            }\n        }\n\n        override fun stopListeningForStats(cb: ISagerNetServiceCallback) {\n            launch {\n                if (statsListeners.remove(cb.asBinder()) != null && statsListeners.isEmpty()) {\n                    statsLooper!!.cancel()\n                    statsLooper = null\n                }\n            }\n        }\n\n        override fun resetTrafficStats() {\n            runOnDefaultDispatcher {\n                SagerDatabase.statsDao.deleteAll()\n                (data?.proxy?.service as? VpnService)?.getTun()?.resetAppTraffics()\n                val empty = AppStatsList(emptyList())\n                broadcast { item ->\n                    if (statsListeners.contains(item.asBinder())) {\n                        item.statsUpdated(empty)\n                    }\n                }\n            }\n        }\n\n        fun stateChanged(s: State, msg: String?) = launch {\n            val profileName = profileName\n            broadcast { it.stateChanged(s.ordinal, profileName, msg) }\n        }\n\n        fun profilePersisted(ids: List<Long>) = launch {\n            if (bandwidthListeners.isNotEmpty() && ids.isNotEmpty()) broadcast { item ->\n                if (bandwidthListeners.contains(item.asBinder())) ids.forEach(item::profilePersisted)\n            }\n        }\n\n        fun missingPlugin(pluginName: String) = launch {\n            val profileName = profileName\n            broadcast { it.missingPlugin(profileName, pluginName) }\n        }\n\n        override fun getTrafficStatsEnabled(): Boolean {\n            return (data?.proxy?.service as? VpnService)?.getTun()?.trafficStatsEnabled ?: false\n        }\n\n        override fun close() {\n            callbacks.kill()\n            cancel()\n            data = null\n        }\n    }\n\n    interface Interface {\n        val data: Data\n        val tag: String\n        fun createNotification(profileName: String): ServiceNotification\n\n        fun onBind(intent: Intent): IBinder? =\n            if (intent.action == Action.SERVICE) data.binder else null\n\n        fun forceLoad() {\n            if (DataStore.selectedProxy == 0L) {\n                stopRunner(false, (this as Context).getString(R.string.profile_empty))\n            }\n            val s = data.state\n            when {\n                s == State.Stopped -> startRunner()\n                s.canStop -> stopRunner(true)\n                else -> Logs.w(\"Illegal state $s when invoking use\")\n            }\n        }\n\n        val isVpnService get() = false\n\n        suspend fun startProcesses() {\n            data.proxy!!.launch()\n        }\n\n        fun startRunner() {\n            this as Context\n            if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))\n            else startService(Intent(this, javaClass))\n        }\n\n        fun killProcesses() {\n            data.proxy?.close()\n        }\n\n        fun stopRunner(restart: Boolean = false, msg: String? = null, keepState: Boolean = true) {\n            if (data.state == State.Stopping) return\n            data.notification?.destroy()\n            data.notification = null\n            this as Service\n\n            data.changeState(State.Stopping)\n\n            runOnMainDispatcher {\n                data.connectingJob?.cancelAndJoin() // ensure stop connecting first\n                // we use a coroutineScope here to allow clean-up in parallel\n                coroutineScope {\n                    killProcesses()\n                    val data = data\n                    if (data.closeReceiverRegistered) {\n                        unregisterReceiver(data.closeReceiver)\n                        data.closeReceiverRegistered = false\n                    }\n                    data.binder.profilePersisted(listOfNotNull(data.proxy).map { it.profile.id })\n                    data.proxy = null\n                }\n\n                // change the state\n                data.changeState(State.Stopped, msg)\n                // stop the service if nothing has bound to it\n                if (restart) startRunner() else { //   BootReceiver.enabled = false\n                    stopSelf()\n                }\n            }\n        }\n\n        fun persistStats() {\n            Logs.w(Exception())\n            data.proxy?.persistStats()\n            (this as? VpnService)?.persistAppStats()\n        }\n\n        suspend fun preInit() {}\n\n        fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n\n            val data = data\n            if (data.state != State.Stopped) return Service.START_NOT_STICKY\n            val profile = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)\n            this as Context\n            if (profile == null) { // gracefully shutdown: https://stackoverflow.com/q/47337857/2245107\n                data.notification = createNotification(\"\")\n                stopRunner(false, getString(R.string.profile_empty))\n                return Service.START_NOT_STICKY\n            }\n            val proxy = ProxyInstance(profile, this)\n            data.proxy = proxy\n            BootReceiver.enabled = DataStore.persistAcrossReboot\n            if (!data.closeReceiverRegistered) {\n                registerReceiver(data.closeReceiver, IntentFilter().apply {\n                    addAction(Action.RELOAD)\n                    addAction(Intent.ACTION_SHUTDOWN)\n                    addAction(Action.CLOSE)\n                }, \"$packageName.SERVICE\", null)\n                data.closeReceiverRegistered = true\n            }\n\n            data.notification = createNotification(profile.displayName())\n\n            data.changeState(State.Connecting)\n            runOnMainDispatcher {\n                try {\n                    Executable.killAll()    // clean up old processes\n                    preInit()\n                    try {\n                        proxy.init()\n                    } catch (jsonEx: JSONException) {\n                        error(jsonEx.readableMessage.replace(\"cn.hutool.json.\", \"\"))\n                    }\n                    proxy.processes = GuardedProcessPool {\n                        Logs.w(it)\n                        stopRunner(false, it.readableMessage)\n                    }\n                    DataStore.currentProfile = profile.id\n                    DataStore.startedProfile = profile.id\n                    startProcesses()\n                    data.changeState(State.Connected)\n\n                    for ((type, routeName) in proxy.config.alerts) {\n                        data.binder.broadcast {\n                            it.routeAlert(type, routeName)\n                        }\n                    }\n                } catch (_: CancellationException) { // if the job was cancelled, it is canceller's responsibility to call stopRunner\n                } catch (_: UnknownHostException) {\n                    stopRunner(false, getString(R.string.invalid_server))\n                } catch (e: PluginManager.PluginNotFoundException) {\n                    Logs.d(e.readableMessage)\n                    data.binder.missingPlugin(e.plugin)\n                    stopRunner(false, null)\n                } catch (e: ShadowsocksPluginPluginManager.PluginNotFoundException) {\n                    Logs.d(e.readableMessage)\n                    data.binder.missingPlugin(\"shadowsocks-\" + e.plugin)\n                    stopRunner(false, null)\n                } catch (exc: Throwable) {\n                    if (exc is ExpectedException) Logs.d(exc.readableMessage) else Logs.w(exc)\n                    stopRunner(\n                        false, \"${getString(R.string.service_failed)}: ${exc.readableMessage}\"\n                    )\n                } finally {\n                    data.connectingJob = null\n                }\n            }\n            return Service.START_NOT_STICKY\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/ClashBasedInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport libcore.ClashBasedInstance\n\nabstract class ClashBasedInstance : AbstractInstance {\n\n    lateinit var instance: ClashBasedInstance\n\n    abstract fun createInstance()\n\n    override fun launch() {\n        createInstance()\n        instance.start()\n    }\n\n    override fun close() {\n        if (::instance.isInitialized) instance.close()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.system.ErrnoException\nimport android.system.Os\nimport android.system.OsConstants\nimport android.text.TextUtils\nimport io.nekohasekai.sagernet.ktx.Logs\nimport java.io.File\nimport java.io.IOException\n\nobject Executable {\n    const val SS_LOCAL = \"libsslocal.so\"\n    const val SS_LIBEV_LOCAL = \"libss-local.so\"\n\n    private val EXECUTABLES = setOf(\n        SS_LOCAL,\n        SS_LIBEV_LOCAL,\n        \"libtrojan.so\",\n        \"libtrojan-go.so\",\n        \"libnaive.so\",\n        \"libbrook.so\",\n        \"libhysteria.so\",\n        \"libpingtunnel.so\",\n        \"librelaybaton.so\",\n        \"libwg.so\"\n    )\n\n    fun killAll() {\n        for (process in File(\"/proc\").listFiles { _, name -> TextUtils.isDigitsOnly(name) }\n            ?: return) {\n            val exe = File(try {\n                File(process, \"cmdline\").inputStream().bufferedReader().use {\n                    it.readText()\n                }\n            } catch (_: IOException) {\n                continue\n            }.split(Character.MIN_VALUE, limit = 2).first())\n            if (EXECUTABLES.contains(exe.name)) try {\n                Os.kill(process.name.toInt(), OsConstants.SIGKILL)\n                Logs.w(\"SIGKILL ${exe.nameWithoutExtension} (${process.name}) succeed\")\n            } catch (e: ErrnoException) {\n                if (e.errno != OsConstants.ESRCH) {\n                    Logs.w(\"SIGKILL ${exe.absolutePath} (${process.name}) failed\")\n                    Logs.w(e)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/ExternalInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport io.nekohasekai.sagernet.bg.proto.V2RayInstance\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.fmt.buildCustomConfig\nimport io.nekohasekai.sagernet.ktx.Logs\n\nclass ExternalInstance(\n    profile: ProxyEntity, val port: Int\n) : V2RayInstance(profile) {\n\n    override fun init() {\n        super.init()\n\n        Logs.d(config.config)\n        pluginConfigs.forEach { (_, plugin) ->\n            val (_, content) = plugin\n            Logs.d(content)\n        }\n    }\n\n    override fun buildConfig() {\n        config = buildCustomConfig(profile, port)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/ForegroundDetectorService.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.accessibilityservice.AccessibilityService\nimport android.view.accessibility.AccessibilityEvent\nimport android.view.inputmethod.InputMethodManager\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport libcore.Libcore\n\nclass ForegroundDetectorService : AccessibilityService() {\n\n    class NotStartedException(val routeName: String) : IllegalStateException()\n\n    val imeApps by lazy {\n        (applicationContext.getSystemService(\n            INPUT_METHOD_SERVICE\n        ) as InputMethodManager).inputMethodList.map { it.packageName }\n    }\n    var fromIme = false\n\n    override fun onCreate() {\n        super.onCreate()\n\n        Logs.i(\"Started\")\n    }\n\n    override fun onAccessibilityEvent(event: AccessibilityEvent) {\n\n        val packageName = event.packageName?.takeIf { it.isNotBlank() }?.toString() ?: return\n        if (packageName == \"com.android.systemui\") return\n        if (packageName in imeApps) {\n            val uid = PackageCache[packageName] ?: return\n            PackageCache.awaitLoadSync()\n            Libcore.setForegroundImeUid(uid)\n            fromIme = true\n\n            Logs.d(\"Foreground IME changed to ${event.packageName}/${event.className}: uid $uid\")\n            return\n        }\n\n        PackageCache.awaitLoadSync()\n        var uid = PackageCache[packageName] ?: -1\n        if (uid < 10000) {\n            uid = 1000\n        }\n\n        Libcore.setForegroundUid(uid)\n        if (fromIme) {\n            Libcore.setForegroundImeUid(0)\n            fromIme = false\n\n            Logs.d(\"Foreground IME changed to none\")\n        }\n\n        Logs.d(\"Foreground changed to ${event.packageName}/${event.className}: uid $uid\")\n    }\n\n    override fun onInterrupt() {\n        Logs.i(\"Interrupted\")\n\n        Libcore.setForegroundUid(0)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.os.Build\nimport android.os.SystemClock\nimport android.system.ErrnoException\nimport android.system.Os\nimport android.system.OsConstants\nimport android.util.Log\nimport androidx.annotation.MainThread\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.utils.Commandline\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport java.io.File\nimport java.io.IOException\nimport java.io.InputStream\nimport kotlin.concurrent.thread\n\nclass GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {\n    companion object {\n        private val pid by lazy {\n            Class.forName(\"java.lang.ProcessManager\\$ProcessImpl\").getDeclaredField(\"pid\")\n                .apply { isAccessible = true }\n        }\n    }\n\n    private inner class Guard(private val cmd: List<String>, private val env: Map<String, String> = mapOf()) {\n        private lateinit var process: Process\n\n        private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {\n            input.bufferedReader().forEachLine(logger)\n        } catch (_: IOException) {\n        }    // ignore\n\n        fun start() {\n            process = ProcessBuilder(cmd).directory(SagerNet.deviceStorage.noBackupFilesDir).apply {\n                environment().putAll(env)\n            }.start()\n        }\n\n        @DelicateCoroutinesApi\n        suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {\n            var running = false\n            val cmdName = File(cmd.first()).nameWithoutExtension\n            val exitChannel = Channel<Int>()\n            try {\n                while (true) {\n                    thread(name = \"stderr-$cmdName\") {\n                        streamLogger(process.errorStream) { Log.e(cmdName, it) }\n                    }\n                    thread(name = \"stdout-$cmdName\") {\n                        streamLogger(process.inputStream) { Log.i(cmdName, it) }\n                        // this thread also acts as a daemon thread for waitFor\n                        runBlocking { exitChannel.send(process.waitFor()) }\n                    }\n                    val startTime = SystemClock.elapsedRealtime()\n                    val exitCode = exitChannel.receive()\n                    running = false\n                    when {\n                        SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException(\n                            \"$cmdName exits too fast (exit code: $exitCode)\")\n                        exitCode == 128 + OsConstants.SIGKILL -> Logs.w(\"$cmdName was killed\")\n                        else -> Logs.w(IOException(\"$cmdName unexpectedly exits with code $exitCode\"))\n                    }\n                    Logs.i(\"restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)\")\n                    start()\n                    running = true\n                    onRestartCallback?.invoke()\n                }\n            } catch (e: IOException) {\n                Logs.w(\"error occurred. stop guard: ${Commandline.toString(cmd)}\")\n                GlobalScope.launch(Dispatchers.Main) { onFatal(e) }\n            } finally {\n                if (running) withContext(NonCancellable) {  // clean-up cannot be cancelled\n                    if (Build.VERSION.SDK_INT < 24) {\n                        try {\n                            Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)\n                        } catch (e: ErrnoException) {\n                            if (e.errno != OsConstants.ESRCH) Logs.w(e)\n                        } catch (e: ReflectiveOperationException) {\n                            Logs.w(e)\n                        }\n                        if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext\n                    }\n                    process.destroy()                       // kill the process\n                    if (Build.VERSION.SDK_INT >= 26) {\n                        if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext\n                        process.destroyForcibly()           // Force to kill the process if it's still alive\n                    }\n                    exitChannel.receive()\n                }                                           // otherwise process already exited, nothing to be done\n            }\n        }\n    }\n\n    override val coroutineContext = Dispatchers.Main.immediate + Job()\n\n    @MainThread\n    fun start(cmd: List<String>,env: Map<String,String> = mapOf(), onRestartCallback: (suspend () -> Unit)? = null) {\n        Logs.i(\"start process: ${Commandline.toString(cmd)}\")\n        Guard(cmd, env).apply {\n            start() // if start fails, IOException will be thrown directly\n            launch { looper(onRestartCallback) }\n        }\n    }\n\n    @MainThread\n    fun close(scope: CoroutineScope) {\n        cancel()\n        coroutineContext[Job]!!.also { job -> scope.launch { job.cancelAndJoin() } }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/ProxyService.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.app.Service\nimport android.content.Intent\n\nclass ProxyService : Service(), BaseService.Interface {\n    override val data = BaseService.Data(this)\n    override val tag: String get() = \"SagerNetProxyService\"\n    override fun createNotification(profileName: String): ServiceNotification =\n        ServiceNotification(this, profileName, \"service-proxy\", true)\n\n    override fun onBind(intent: Intent) = super.onBind(intent)\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =\n        super<BaseService.Interface>.onStartCommand(intent, flags, startId)\n\n    override fun onDestroy() {\n        super.onDestroy()\n        data.binder.close()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.ServiceConnection\nimport android.os.IBinder\nimport android.os.RemoteException\nimport io.nekohasekai.sagernet.Action\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.aidl.*\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.runOnMainDispatcher\n\nclass SagerConnection(private var listenForDeath: Boolean = false) : ServiceConnection,\n    IBinder.DeathRecipient {\n    companion object {\n        val serviceClass\n            get() = when (DataStore.serviceMode) {\n                Key.MODE_PROXY -> ProxyService::class\n                Key.MODE_VPN -> VpnService::class //   Key.MODE_TRANS -> TransproxyService::class\n                else -> throw UnknownError()\n            }.java\n    }\n\n    interface Callback {\n        fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)\n        fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {}\n        fun statsUpdated(stats: List<AppStats>) {}\n        fun observatoryResultsUpdated(groupId: Long) {}\n\n        fun profilePersisted(profileId: Long) {}\n        fun missingPlugin(profileName: String, pluginName: String) {}\n        fun routeAlert(type: Int, routeName: String) {}\n\n        fun onServiceConnected(service: ISagerNetService)\n\n        /**\n         * Different from Android framework, this method will be called even when you call `detachService`.\n         */\n        fun onServiceDisconnected() {}\n        fun onBinderDied() {}\n    }\n\n    private var connectionActive = false\n    private var callbackRegistered = false\n    private var callback: Callback? = null\n    private val serviceCallback = object : ISagerNetServiceCallback.Stub() {\n        override fun stateChanged(state: Int, profileName: String?, msg: String?) {\n            val s = BaseService.State.values()[state]\n            SagerNet.started = s.canStop\n            val callback = callback ?: return\n            runOnMainDispatcher {\n                callback.stateChanged(s, profileName, msg)\n            }\n        }\n\n        override fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {\n            val callback = callback ?: return\n            runOnMainDispatcher {\n                callback.trafficUpdated(profileId, stats, isCurrent)\n            }\n        }\n\n        override fun profilePersisted(profileId: Long) {\n            val callback = callback ?: return\n            runOnMainDispatcher { callback.profilePersisted(profileId) }\n        }\n\n        override fun missingPlugin(profileName: String, pluginName: String) {\n            val callback = callback ?: return\n            runOnMainDispatcher {\n                callback.missingPlugin(profileName, pluginName)\n            }\n        }\n\n        override fun statsUpdated(statsList: AppStatsList) {\n            val callback = callback ?: return\n            callback.statsUpdated(statsList.data)\n        }\n\n        override fun routeAlert(type: Int, routeName: String) {\n            val callback = callback ?: return\n            runOnMainDispatcher {\n                callback.routeAlert(type, routeName)\n            }\n        }\n\n        override fun observatoryResultsUpdated(groupId: Long) {\n            val callback = callback ?: return\n            runOnMainDispatcher {\n                callback.observatoryResultsUpdated(groupId)\n            }\n        }\n    }\n\n    private var binder: IBinder? = null\n\n    var bandwidthTimeout = 0L\n        set(value) {\n            try {\n                if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)\n                else service?.stopListeningForBandwidth(serviceCallback)\n            } catch (_: RemoteException) {\n            }\n            field = value\n        }\n    var trafficTimeout = 0L\n        set(value) {\n            try {\n                if (value > 0) service?.startListeningForStats(serviceCallback, value)\n                else service?.stopListeningForStats(serviceCallback)\n            } catch (_: RemoteException) {\n            }\n            field = value\n        }\n    var service: ISagerNetService? = null\n\n    override fun onServiceConnected(name: ComponentName?, binder: IBinder) {\n        this.binder = binder\n        val service = ISagerNetService.Stub.asInterface(binder)!!\n        this.service = service\n        try {\n            if (listenForDeath) binder.linkToDeath(this, 0)\n            check(!callbackRegistered)\n            service.registerCallback(serviceCallback)\n            callbackRegistered = true\n            if (bandwidthTimeout > 0) service.startListeningForBandwidth(\n                serviceCallback, bandwidthTimeout\n            )\n            if (trafficTimeout > 0) service.startListeningForStats(\n                serviceCallback, trafficTimeout\n            )\n        } catch (e: RemoteException) {\n            e.printStackTrace()\n        }\n        callback!!.onServiceConnected(service)\n    }\n\n    override fun onServiceDisconnected(name: ComponentName?) {\n        unregisterCallback()\n        callback?.onServiceDisconnected()\n        service = null\n        binder = null\n    }\n\n    override fun binderDied() {\n        service = null\n        callbackRegistered = false\n        callback?.also { runOnMainDispatcher { it.onBinderDied() } }\n    }\n\n    private fun unregisterCallback() {\n        val service = service\n        if (service != null && callbackRegistered) try {\n            service.unregisterCallback(serviceCallback)\n        } catch (_: RemoteException) {\n        }\n        callbackRegistered = false\n    }\n\n    fun connect(context: Context, callback: Callback) {\n        if (connectionActive) return\n        connectionActive = true\n        check(this.callback == null)\n        this.callback = callback\n        val intent = Intent(context, serviceClass).setAction(Action.SERVICE)\n        context.bindService(intent, this, Context.BIND_AUTO_CREATE)\n    }\n\n    fun disconnect(context: Context) {\n        unregisterCallback()\n        if (connectionActive) try {\n            context.unbindService(this)\n        } catch (_: IllegalArgumentException) {\n        }   // ignore\n        connectionActive = false\n        if (listenForDeath) try {\n            binder?.unlinkToDeath(this, 0)\n        } catch (_: NoSuchElementException) {\n        }\n        binder = null\n        try {\n            service?.stopListeningForBandwidth(serviceCallback)\n            service?.stopListeningForStats(serviceCallback)\n        } catch (_: RemoteException) {\n        }\n        service = null\n        callback = null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/ServiceNotification.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.os.Build\nimport android.os.PowerManager\nimport android.text.format.Formatter\nimport androidx.core.app.NotificationCompat\nimport androidx.core.content.getSystemService\nimport io.nekohasekai.sagernet.Action\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.aidl.AppStatsList\nimport io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback\nimport io.nekohasekai.sagernet.aidl.TrafficStats\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.getColorAttr\nimport io.nekohasekai.sagernet.utils.Theme\n\n/**\n * User can customize visibility of notification since Android 8.\n * The default visibility:\n *\n * Android 8.x: always visible due to system limitations\n * VPN:         always invisible because of VPN notification/icon\n * Other:       always visible\n *\n * See also: https://github.com/aosp-mirror/platform_frameworks_base/commit/070d142993403cc2c42eca808ff3fafcee220ac4\n */\nclass ServiceNotification(\n    private val service: BaseService.Interface, profileName: String,\n    channel: String, visible: Boolean = false,\n) : BroadcastReceiver() {\n    val trafficStatistics = DataStore.profileTrafficStatistics\n    val showDirectSpeed = DataStore.showDirectSpeed\n\n    private val callback: ISagerNetServiceCallback by lazy {\n        object : ISagerNetServiceCallback.Stub() {\n            override fun stateChanged(state: Int, profileName: String?, msg: String?) {}   // ignore\n            override fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {\n                if (!trafficStatistics || profileId == 0L || !isCurrent) return\n                builder.apply {\n                    if (showDirectSpeed) {\n                        val speedDetail = (service as Context).getString(\n                            R.string.speed_detail, service.getString(\n                                R.string.speed, Formatter.formatFileSize(service, stats.txRateProxy)\n                            ), service.getString(\n                                R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy)\n                            ), service.getString(\n                                R.string.speed,\n                                Formatter.formatFileSize(service, stats.txRateDirect)\n                            ), service.getString(\n                                R.string.speed,\n                                Formatter.formatFileSize(service, stats.rxRateDirect)\n                            )\n                        )\n                        setStyle(NotificationCompat.BigTextStyle().bigText(speedDetail))\n                        setContentText(speedDetail)\n                    } else {\n                        val speedSimple = (service as Context).getString(\n                            R.string.traffic, service.getString(\n                                R.string.speed, Formatter.formatFileSize(service, stats.txRateProxy)\n                            ), service.getString(\n                                R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy)\n                            )\n                        )\n                        setContentText(speedSimple)\n                    }\n                    setSubText(\n                        service.getString(\n                            R.string.traffic,\n                            Formatter.formatFileSize(service, stats.txTotal),\n                            Formatter.formatFileSize(service, stats.rxTotal)\n                        )\n                    )\n                }\n                show()\n            }\n\n            override fun statsUpdated(statsList: AppStatsList?) {\n            }\n\n            override fun observatoryResultsUpdated(groupId: Long) {\n            }\n\n            override fun profilePersisted(profileId: Long) {\n            }\n\n            override fun missingPlugin(profileName: String?, pluginName: String?) {\n            }\n\n            override fun routeAlert(type: Int, routeName: String?) {\n            }\n        }\n    }\n    private var callbackRegistered = false\n\n    private val builder = NotificationCompat.Builder(service as Context, channel).setWhen(0)\n        .setTicker(service.getString(R.string.forward_success)).setContentTitle(profileName)\n        .setContentIntent(SagerNet.configureIntent(service))\n        .setSmallIcon(R.drawable.ic_service_ax).setCategory(NotificationCompat.CATEGORY_SERVICE)\n        .setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)\n    init {\n        service as Context\n        val closeAction = NotificationCompat.Action.Builder(\n            R.drawable.ic_navigation_close,\n            service.getText(R.string.stop),\n            PendingIntent.getBroadcast(\n                service,\n                0,\n                Intent(Action.CLOSE).setPackage(service.packageName),\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0\n            )\n        ).apply {\n            setShowsUserInterface(false)\n        }.build()\n        if (Build.VERSION.SDK_INT < 24 || DataStore.showStopButton) builder.addAction(closeAction) else builder.addInvisibleAction(\n            closeAction\n        )\n        Theme.apply(app)\n        Theme.apply(service)\n        builder.color = service.getColorAttr(R.attr.colorPrimary)\n\n        updateCallback(service.getSystemService<PowerManager>()?.isInteractive != false)\n        service.registerReceiver(this, IntentFilter().apply {\n            addAction(Intent.ACTION_SCREEN_ON)\n            addAction(Intent.ACTION_SCREEN_OFF)\n        })\n        show()\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        if (service.data.state == BaseService.State.Connected) updateCallback(intent.action == Intent.ACTION_SCREEN_ON)\n    }\n\n    private fun updateCallback(screenOn: Boolean) {\n        if (!trafficStatistics) return\n        if (screenOn) {\n            service.data.binder.registerCallback(callback)\n            service.data.binder.startListeningForBandwidth(\n                callback, DataStore.speedInterval.toLong()\n            )\n            callbackRegistered = true\n        } else if (callbackRegistered) {    // unregister callback to save battery\n            service.data.binder.unregisterCallback(callback)\n            callbackRegistered = false\n        }\n    }\n\n    private fun show() = (service as Service).startForeground(1, builder.build())\n\n    fun destroy() {\n        (service as Service).stopForeground(true)\n        service.unregisterReceiver(this)\n        updateCallback(false)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/SubscriptionUpdater.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.content.Context\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.work.CoroutineWorker\nimport androidx.work.ExistingPeriodicWorkPolicy\nimport androidx.work.PeriodicWorkRequest\nimport androidx.work.WorkerParameters\nimport androidx.work.multiprocess.RemoteWorkManager\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.group.GroupUpdater\nimport io.nekohasekai.sagernet.ktx.app\nimport java.util.concurrent.TimeUnit\n\nobject SubscriptionUpdater {\n\n    private const val WORK_NAME = \"SubscriptionUpdater\"\n\n    suspend fun reconfigureUpdater() {\n        RemoteWorkManager.getInstance(app).cancelUniqueWork(WORK_NAME)\n\n        val subscriptions = SagerDatabase.groupDao.subscriptions()\n            .filter { it.subscription!!.autoUpdate }\n        if (subscriptions.isEmpty()) return\n\n        // PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS\n        var minDelay = subscriptions.minByOrNull { it.subscription!!.autoUpdateDelay }!!.subscription!!.autoUpdateDelay.toLong()\n        val now = System.currentTimeMillis() / 1000L\n        val minInitDelay = subscriptions.minOf { now - it.subscription!!.lastUpdated - (minDelay * 60) }\n        if (minDelay < 15) minDelay = 15\n\n        RemoteWorkManager.getInstance(app).enqueueUniquePeriodicWork(\n            WORK_NAME,\n            ExistingPeriodicWorkPolicy.REPLACE,\n            PeriodicWorkRequest.Builder(UpdateTask::class.java, minDelay, TimeUnit.MINUTES)\n                .apply {\n                    if (minInitDelay > 0) setInitialDelay(minInitDelay, TimeUnit.SECONDS)\n                }\n                .build()\n        )\n    }\n\n    class UpdateTask(\n        appContext: Context, params: WorkerParameters\n    ) : CoroutineWorker(appContext, params) {\n\n        val nm = NotificationManagerCompat.from(applicationContext)\n\n        val notification = NotificationCompat.Builder(applicationContext, \"service-subscription\")\n            .setWhen(0)\n            .setTicker(applicationContext.getString(R.string.forward_success))\n            .setContentTitle(applicationContext.getString(R.string.subscription_update))\n            .setSmallIcon(R.drawable.ic_service_active)\n            .setCategory(NotificationCompat.CATEGORY_SERVICE)\n\n        override suspend fun doWork(): Result {\n            var subscriptions = SagerDatabase.groupDao.subscriptions()\n                .filter { it.subscription!!.autoUpdate }\n            if (DataStore.startedProfile == 0L) {\n                subscriptions = subscriptions.filter { !it.subscription!!.updateWhenConnectedOnly }\n            }\n\n            if (subscriptions.isNotEmpty()) for (profile in subscriptions) {\n                val subscription = profile.subscription!!\n\n                if (((System.currentTimeMillis() / 1000).toInt() - subscription.lastUpdated) < subscription.autoUpdateDelay * 60) {\n                    continue\n                }\n\n                notification.setContentText(\n                    applicationContext.getString(\n                        R.string.subscription_update_message, profile.displayName()\n                    )\n                )\n                nm.notify(2, notification.build())\n\n                GroupUpdater.executeUpdate(profile, false)\n            }\n\n            nm.cancel(2)\n\n            return Result.success()\n        }\n    }\n\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt",
    "content": "/*******************************************************************************\n *                                                                             *\n *  Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com>                          *\n *  Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                             *\n *******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.app.KeyguardManager\nimport android.graphics.drawable.Icon\nimport android.service.quicksettings.Tile\nimport androidx.annotation.RequiresApi\nimport androidx.core.content.getSystemService\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.aidl.ISagerNetService\nimport io.nekohasekai.sagernet.database.DataStore\nimport android.service.quicksettings.TileService as BaseTileService\n\n@RequiresApi(24)\nclass TileService : BaseTileService(), SagerConnection.Callback {\n    private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_ax) }\n    private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }\n    private val iconConnected by lazy {\n        Icon.createWithResource(this,\n            R.drawable.ic_service_active)\n    }\n    private val keyguard by lazy { getSystemService<KeyguardManager>()!! }\n    private var tapPending = false\n\n    private val connection = SagerConnection()\n    override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =\n        updateTile(state) { profileName }\n\n    override fun onServiceConnected(service: ISagerNetService) {\n        updateTile(BaseService.State.values()[service.state]) { service.profileName }\n        if (tapPending) {\n            tapPending = false\n            onClick()\n        }\n    }\n\n    override fun onStartListening() {\n        super.onStartListening()\n        connection.connect(this, this)\n    }\n\n    override fun onStopListening() {\n        connection.disconnect(this)\n        super.onStopListening()\n    }\n\n    override fun onClick() {\n        if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()\n    }\n\n    private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {\n        qsTile?.apply {\n            label = null\n            when (serviceState) {\n                BaseService.State.Idle -> error(\"serviceState\")\n                BaseService.State.Connecting -> {\n                    icon = iconIdle\n                    state = Tile.STATE_ACTIVE\n                }\n                BaseService.State.Connected -> {\n                    icon = iconIdle\n                    if (!keyguard.isDeviceLocked) label = profileName()\n                    state = Tile.STATE_ACTIVE\n                }\n                BaseService.State.Stopping -> {\n                    icon = iconIdle\n                    state = Tile.STATE_UNAVAILABLE\n                }\n                BaseService.State.Stopped -> {\n                    icon = iconIdle\n                    state = Tile.STATE_INACTIVE\n                }\n            }\n            label = label ?: getString(R.string.app_name)\n            updateTile()\n        }\n    }\n\n    private fun toggle() {\n        val service = connection.service\n        if (service == null) tapPending =\n            true else BaseService.State.values()[service.state].let { state ->\n            when {\n                state.canStop -> SagerNet.stopService()\n                state == BaseService.State.Stopped -> SagerNet.startService()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg\n\nimport android.Manifest\nimport android.app.Service\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Network\nimport android.net.ProxyInfo\nimport android.os.Build\nimport android.os.ParcelFileDescriptor\nimport android.system.ErrnoException\nimport android.system.Os\nimport androidx.annotation.RequiresApi\nimport io.nekohasekai.sagernet.*\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.database.StatsEntity\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ui.VpnRequestActivity\nimport io.nekohasekai.sagernet.utils.DefaultNetworkListener\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport io.nekohasekai.sagernet.utils.Subnet\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\nimport libcore.AppStats\nimport libcore.Libcore\nimport libcore.TrafficListener\nimport libcore.Tun2ray\nimport java.io.FileDescriptor\nimport android.net.VpnService as BaseVpnService\n\nclass VpnService : BaseVpnService(),\n    BaseService.Interface,\n    TrafficListener {\n\n    companion object {\n        var instance: VpnService? = null\n\n        const val VPN_MTU = 1500\n        const val PRIVATE_VLAN4_CLIENT = \"172.19.0.1\"\n        const val PRIVATE_VLAN4_ROUTER = \"172.19.0.2\"\n        const val FAKEDNS_VLAN4_CLIENT = \"198.18.0.0\"\n        const val PRIVATE_VLAN6_CLIENT = \"fdfe:dcba:9876::1\"\n        const val PRIVATE_VLAN6_ROUTER = \"fdfe:dcba:9876::2\"\n        const val FAKEDNS_VLAN6_CLIENT = \"fc00::\"\n\n        private fun <T> FileDescriptor.use(block: (FileDescriptor) -> T) = try {\n            block(this)\n        } finally {\n            try {\n                Os.close(this)\n            } catch (_: ErrnoException) {\n            }\n        }\n    }\n\n    lateinit var conn: ParcelFileDescriptor\n    private lateinit var tun: Tun2ray\n    fun getTun(): Tun2ray? {\n        if (!::tun.isInitialized) return null\n        return tun\n    }\n\n    private var active = false\n    private var metered = false\n\n    @Volatile\n    private var underlyingNetwork: Network? = null\n        @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) set(value) {\n            field = value\n            if (active && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {\n                setUnderlyingNetworks(underlyingNetworks)\n            }\n        }\n    private val underlyingNetworks\n        get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered\n            if (Build.VERSION.SDK_INT == 28 && metered) null else underlyingNetwork?.let {\n                arrayOf(it)\n            }\n\n    override suspend fun startProcesses() {\n        super.startProcesses()\n        startVpn()\n    }\n\n    @Suppress(\"EXPERIMENTAL_API_USAGE\")\n    override fun killProcesses() {\n        getTun()?.close()\n        if (::conn.isInitialized) conn.close()\n        super.killProcesses()\n        persistAppStats()\n        active = false\n        GlobalScope.launch(Dispatchers.Default) { DefaultNetworkListener.stop(this) }\n    }\n\n    override fun onBind(intent: Intent) = when (intent.action) {\n        SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)\n        else -> super<BaseService.Interface>.onBind(intent)\n    }\n\n    override val data = BaseService.Data(this)\n    override val tag = \"SagerNetVpnService\"\n    override fun createNotification(profileName: String) =\n        ServiceNotification(this, profileName, \"service-vpn\")\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        if (DataStore.serviceMode == Key.MODE_VPN) {\n            if (prepare(this) != null) {\n                startActivity(\n                    Intent(\n                        this, VpnRequestActivity::class.java\n                    ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                )\n            } else return super<BaseService.Interface>.onStartCommand(intent, flags, startId)\n        }\n        stopRunner()\n        return Service.START_NOT_STICKY\n    }\n\n    override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }\n\n    inner class NullConnectionException : NullPointerException(),\n        BaseService.ExpectedException {\n        override fun getLocalizedMessage() = getString(R.string.reboot_required)\n    }\n\n    private fun startVpn() {\n        instance = this\n\n        val profile = data.proxy!!.profile\n        val builder = Builder().setConfigureIntent(SagerNet.configureIntent(this))\n            .setSession(profile.displayName())\n            .setMtu(VPN_MTU)\n        val useFakeDns = DataStore.enableFakeDns\n        val ipv6Mode = DataStore.ipv6Mode\n\n        builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)\n        if (ipv6Mode != IPv6Mode.DISABLE) {\n            builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)\n        }\n        if (useFakeDns) {\n            if (ipv6Mode != IPv6Mode.ONLY) {\n                builder.addAddress(FAKEDNS_VLAN4_CLIENT, 15)\n            } else {\n                builder.addAddress(FAKEDNS_VLAN6_CLIENT, 18)\n            }\n        }\n\n        if (DataStore.bypassLan && !DataStore.bypassLanInCoreOnly) {\n            resources.getStringArray(R.array.bypass_private_route).forEach {\n                val subnet = Subnet.fromString(it)!!\n                builder.addRoute(subnet.address.hostAddress!!, subnet.prefixSize)\n            }\n            builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)\n            // https://issuetracker.google.com/issues/149636790\n            if (ipv6Mode != IPv6Mode.DISABLE) {\n                builder.addRoute(\"2000::\", 3)\n            }\n        } else {\n            builder.addRoute(\"0.0.0.0\", 0)\n            if (ipv6Mode != IPv6Mode.DISABLE) {\n                builder.addRoute(\"::\", 0)\n            }\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {\n            builder.setUnderlyingNetworks(underlyingNetworks)\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(metered)\n\n        val packageName = packageName\n        val proxyApps = DataStore.proxyApps\n        val needBypassRootUid = data.proxy!!.config.outboundTagsAll.values.any { it.ptBean != null }\n        val needIncludeSelf = data.proxy!!.config.index.any { !it.isBalancer && it.chain.size > 1 }\n        if (proxyApps || needBypassRootUid) {\n            var bypass = DataStore.bypass\n            val individual = mutableSetOf<String>()\n            val allApps by lazy {\n                packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS).filter {\n                    when (it.packageName) {\n                        packageName -> false\n                        \"android\" -> true\n                        else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true\n                    }\n                }.map {\n                    it.packageName\n                }\n            }\n            if (proxyApps) {\n                individual.addAll(DataStore.individual.split('\\n').filter { it.isNotBlank() })\n                if (bypass && needBypassRootUid) {\n                    val individualNew = allApps.toMutableList()\n                    individualNew.removeAll(individual)\n                    individual.clear()\n                    individual.addAll(individualNew)\n                    bypass = false\n                }\n            } else {\n                individual.addAll(allApps)\n                bypass = false\n            }\n\n            individual.apply {\n                if (bypass xor needIncludeSelf) add(packageName) else remove(packageName)\n            }.forEach {\n                try {\n                    if (bypass) {\n                        builder.addDisallowedApplication(it)\n                        Logs.d(\"Add bypass: $it\")\n                    } else {\n                        builder.addAllowedApplication(it)\n                        Logs.d(\"Add allow: $it\")\n                    }\n                } catch (ex: PackageManager.NameNotFoundException) {\n                    Logs.w(ex)\n                }\n            }\n        } else {\n            builder.addDisallowedApplication(packageName)\n        }\n\n        builder.addDnsServer(PRIVATE_VLAN4_ROUTER)\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && DataStore.appendHttpProxy && DataStore.requireHttp) {\n            builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOCALHOST, DataStore.httpPort))\n        }\n\n        metered = DataStore.meteredNetwork\n        active = true   // possible race condition here?\n        if (Build.VERSION.SDK_INT >= 29) builder.setMetered(metered)\n        conn = builder.establish() ?: throw NullConnectionException()\n\n        tun = Libcore.newTun2ray(\n            conn.fd,\n            VPN_MTU,\n            data.proxy!!.v2rayPoint,\n            PRIVATE_VLAN4_ROUTER,\n            DataStore.tunImplementation == TunImplementation.GVISOR,\n            true,\n            DataStore.trafficSniffing,\n            DataStore.destinationOverride,\n            DataStore.enableFakeDns,\n            DataStore.enableLog,\n            data.proxy!!.config.dumpUid,\n            DataStore.appTrafficStatistics,\n            DataStore.enablePcap\n        )\n    }\n\n    val appStats = mutableListOf<AppStats>()\n\n    override fun updateStats(stats: AppStats) {\n        appStats.add(stats)\n    }\n\n    fun persistAppStats() {\n        if (!DataStore.appTrafficStatistics) return\n        val tun = getTun() ?: return\n        appStats.clear()\n        tun.readAppTraffics(this)\n        val toUpdate = mutableListOf<StatsEntity>()\n        val all = SagerDatabase.statsDao.all().associateBy { it.packageName }\n        for (stats in appStats) {\n            val packageName = if (stats.uid >= 10000) {\n                PackageCache.uidMap[stats.uid]?.iterator()?.next() ?: \"android\"\n            } else {\n                \"android\"\n            }\n            if (!all.containsKey(packageName)) {\n                SagerDatabase.statsDao.create(\n                    StatsEntity(\n                        packageName = packageName,\n                        tcpConnections = stats.tcpConnTotal,\n                        udpConnections = stats.udpConnTotal,\n                        uplink = stats.uplinkTotal,\n                        downlink = stats.downlinkTotal\n                    )\n                )\n            } else {\n                val entity = all[packageName]!!\n                entity.tcpConnections += stats.tcpConnTotal\n                entity.udpConnections += stats.udpConnTotal\n                entity.uplink += stats.uplinkTotal\n                entity.downlink += stats.downlinkTotal\n                toUpdate.add(entity)\n            }\n            if (toUpdate.isNotEmpty()) {\n                SagerDatabase.statsDao.update(toUpdate)\n            }\n\n        }\n    }\n\n    override fun onRevoke() = stopRunner()\n\n    override fun onDestroy() {\n        super.onDestroy()\n        data.binder.close()\n    }\n\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/ApiInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport android.os.Build\nimport android.provider.Settings\nimport io.nekohasekai.sagernet.bg.AbstractInstance\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.app\nimport libcore.ApiInstance\n\nclass ApiInstance : AbstractInstance {\n\n    lateinit var point: ApiInstance\n\n    override fun launch() {\n        var deviceName = Settings.Secure.getString(app.contentResolver, \"bluetooth_name\")\n        if (deviceName.isNullOrBlank()) {\n            deviceName = Build.DEVICE\n            if (!deviceName.startsWith(Build.MANUFACTURER)) {\n                deviceName = Build.MANUFACTURER + \" \" + deviceName\n            }\n        }\n        point = ApiInstance(\n            deviceName,\n            DataStore.socksPort,\n            DataStore.localDNSPort,\n            DataStore.enableLog,\n            DataStore.bypassLan\n        )\n        point.start()\n    }\n\n    override fun close() {\n        if (::point.isInitialized) point.close()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\n//import io.nekohasekai.sagernet.BuildConfig\n//import io.nekohasekai.sagernet.bg.test.DebugInstance\n//import com.xray.app.observatory.ObservationResult\n//import com.xray.app.observatory.OutboundStatus\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.bg.VpnService\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.utils.DirectBoot\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.runBlocking\nimport libcore.Libcore\nimport java.io.IOException\n\n\nclass ProxyInstance(profile: ProxyEntity, val service: BaseService.Interface) : V2RayInstance(\n    profile\n) {\n\n    lateinit var observatoryJob: Job\n\n    override fun init() {\n        if (service is VpnService) {\n            Libcore.setProtector { service.protect(it) }\n        } else {\n            Libcore.setProtector { true }\n        }\n\n        super.init()\n\n        Logs.d(config.config)\n        pluginConfigs.forEach { (_, plugin) ->\n            val (_, content) = plugin\n            Logs.d(content)\n        }\n    }\n\n    override fun launch() {\n        super.launch()\n\n        /* if (config.observatoryTags.isNotEmpty()) {\n             observatoryJob = runOnDefaultDispatcher {\n                 sendInitStatuses()\n\n                 val interval = 10000L\n                 while (isActive) {\n                     try {\n                         loopObservatoryResults()\n                     } catch (e: Exception) {\n                         if (e.message?.contains(\"unavailable\") == false) {\n                             Logs.w(e)\n                         }\n                         break\n                     }\n                     delay(interval)\n                 }\n             }\n         }*/\n\n        if (DataStore.allowAccess) {\n            val api = ApiInstance()\n            try {\n                api.launch()\n                externalInstances[11451] = api\n            } catch (e: Exception) {\n                Logs.w(\"Failed to start api server\", e)\n            }\n        }\n\n        /* if (BuildConfig.DEBUG && DataStore.enableLog) {\n             externalInstances[9999] = DebugInstance().apply {\n                 launch()\n             }\n         }*/\n\n        SagerNet.started = true\n    }\n\n    fun sendInitStatuses() {\n        /*val time = (System.currentTimeMillis() / 1000) - 300\n        for (observatoryTag in config.observatoryTags) {\n            val profileId = observatoryTag.substringAfter(\"global-\")\n            if (NumberUtil.isLong(profileId)) {\n                val id = profileId.toLong()\n                val profile = when {\n                    id == profile.id -> profile\n                    statsOutbounds.containsKey(id) -> statsOutbounds[id]!!.proxyEntity\n                    else -> SagerDatabase.proxyDao.getById(id)\n                } ?: continue\n\n                if (profile.status > 0) v2rayPoint.updateStatus(\n                    observatoryTag,\n                    OutboundStatus.newBuilder()\n                        .setOutboundTag(observatoryTag)\n                        .setAlive(profile.status == 1)\n                        .setDelay(profile.ping.toLong())\n                        .setLastErrorReason(profile.error ?: \"\")\n                        .setLastTryTime(time)\n                        .setLastSeenTime(time)\n                        .build()\n                        .toByteArray()\n                )\n            }\n        }*/\n    }\n\n    /* suspend fun loopObservatoryResults() {\n         val statusPb = v2rayPoint.observatoryStatus\n         if (statusPb == null || statusPb.isEmpty()) {\n             return\n         }\n         val statusList = ObservationResult.parseFrom(statusPb)\n         val notify = mutableSetOf<Long>()\n         for (status in statusList.statusList) {\n             val profileId = status.outboundTag.substringAfter(\"global-\")\n             if (NumberUtil.isLong(profileId)) {\n                 val id = profileId.toLong()\n                 var flush = false\n                 val profile = when {\n                     id == profile.id -> profile\n                     statsOutbounds.containsKey(id) -> statsOutbounds[id]!!.proxyEntity\n                     else -> {\n                         flush = true\n                         SagerDatabase.proxyDao.getById(id)\n                     }\n                 }\n\n                 if (profile != null) {\n                     val newStatus = if (status.alive) 1 else 3\n                     val newDelay = status.delay.toInt()\n                     val newErrorReason = status.lastErrorReason\n\n                     if (profile.status != newStatus || profile.ping != newDelay || profile.error != newErrorReason) {\n                         profile.status = newStatus\n                         profile.ping = newDelay\n                         profile.error = newErrorReason\n\n                         notify.add(profile.groupId)\n                         if (flush) SagerDatabase.proxyDao.updateProxy(profile)\n\n                         Logs.d(\"Send result for #$profileId ${profile.displayName()}\")\n                     }\n                 } else {\n                     Logs.d(\"Profile with id #$profileId not found\")\n                 }\n             } else {\n                 Logs.d(\"Persist skipped on outbound ${status.outboundTag}\")\n             }\n         }\n         if (notify.isNotEmpty()) {\n             onMainDispatcher {\n                 service.data.binder.broadcast {\n                     for (groupId in notify) it.observatoryResultsUpdated(groupId)\n                 }\n             }\n         }\n     }*/\n\n    override fun close() {\n        SagerNet.started = false\n\n        persistStats()\n        super.close()\n\n        if (::observatoryJob.isInitialized) observatoryJob.cancel()\n    }\n\n    // ------------- stats -------------\n\n    private suspend fun queryStats(tag: String, direct: String): Long {\n        return v2rayPoint.queryStats(tag, direct)\n    }\n\n    private val currentTags by lazy {\n        mapOf(* config.outboundTagsCurrent.map {\n            it to config.outboundTagsAll[it]\n        }.toTypedArray())\n    }\n\n    private val statsTags by lazy {\n        mapOf(*  config.outboundTags.toMutableList().apply {\n            removeAll(config.outboundTagsCurrent)\n        }.map {\n            it to config.outboundTagsAll[it]\n        }.toTypedArray())\n    }\n\n    private val interTags by lazy {\n        config.outboundTagsAll.filterKeys { !config.outboundTags.contains(it) }\n    }\n\n    class OutboundStats(\n        val proxyEntity: ProxyEntity, var uplinkTotal: Long = 0L, var downlinkTotal: Long = 0L\n    )\n\n    private val statsOutbounds = hashMapOf<Long, OutboundStats>()\n    private fun registerStats(\n        proxyEntity: ProxyEntity, uplink: Long? = null, downlink: Long? = null\n    ) {\n        if (proxyEntity.id == outboundStats.proxyEntity.id) return\n        val stats = statsOutbounds.getOrPut(proxyEntity.id) {\n            OutboundStats(proxyEntity)\n        }\n        if (uplink != null) {\n            stats.uplinkTotal += uplink\n        }\n        if (downlink != null) {\n            stats.downlinkTotal += downlink\n        }\n    }\n\n    var uplinkProxy = 0L\n    var downlinkProxy = 0L\n    var uplinkTotalDirect = 0L\n    var downlinkTotalDirect = 0L\n\n    private val outboundStats = OutboundStats(profile)\n    suspend fun outboundStats(): Pair<OutboundStats, HashMap<Long, OutboundStats>> {\n        if (!isInitialized()) return outboundStats to statsOutbounds\n        uplinkProxy = 0L\n        downlinkProxy = 0L\n\n        val currentUpLink = currentTags.map { (tag, profile) ->\n            queryStats(tag, \"uplink\").apply { profile?.also { registerStats(it, uplink = this) } }\n        }\n        val currentDownLink = currentTags.map { (tag, profile) ->\n            queryStats(tag, \"downlink\").apply {\n                profile?.also {\n                    registerStats(it, downlink = this)\n                }\n            }\n        }\n        uplinkProxy += currentUpLink.fold(0L) { acc, l -> acc + l }\n        downlinkProxy += currentDownLink.fold(0L) { acc, l -> acc + l }\n\n        outboundStats.uplinkTotal += uplinkProxy\n        outboundStats.downlinkTotal += downlinkProxy\n\n        if (statsTags.isNotEmpty()) {\n            uplinkProxy += statsTags.map { (tag, profile) ->\n                queryStats(tag, \"uplink\").apply {\n                    profile?.also {\n                        registerStats(it, uplink = this)\n                    }\n                }\n            }.fold(0L) { acc, l -> acc + l }\n            downlinkProxy += statsTags.map { (tag, profile) ->\n                queryStats(tag, \"downlink\").apply {\n                    profile?.also {\n                        registerStats(it, downlink = this)\n                    }\n                }\n            }.fold(0L) { acc, l -> acc + l }\n        }\n\n        if (interTags.isNotEmpty()) {\n            interTags.map { (tag, profile) ->\n                queryStats(tag, \"uplink\").also { registerStats(profile, uplink = it) }\n            }\n            interTags.map { (tag, profile) ->\n                queryStats(tag, \"downlink\").also {\n                    registerStats(profile, downlink = it)\n                }\n            }\n        }\n\n        return outboundStats to statsOutbounds\n    }\n\n    suspend fun bypassStats(direct: String): Long {\n        if (!isInitialized()) return 0L\n        return queryStats(config.bypassTag, direct)\n    }\n\n    suspend fun uplinkDirect() = bypassStats(\"uplink\").also {\n        uplinkTotalDirect += it\n    }\n\n    suspend fun downlinkDirect() = bypassStats(\"downlink\").also {\n        downlinkTotalDirect += it\n    }\n\n    fun persistStats() {\n        runBlocking {\n            try {\n                outboundStats()\n\n                val toUpdate = mutableListOf<ProxyEntity>()\n                if (outboundStats.uplinkTotal + outboundStats.downlinkTotal != 0L) {\n                    profile.tx += outboundStats.uplinkTotal\n                    profile.rx += outboundStats.downlinkTotal\n                    toUpdate.add(profile)\n                }\n\n                statsOutbounds.values.forEach {\n                    if (it.uplinkTotal + it.downlinkTotal != 0L) {\n                        it.proxyEntity.tx += it.uplinkTotal\n                        it.proxyEntity.rx += it.downlinkTotal\n                        toUpdate.add(it.proxyEntity)\n                    }\n                }\n\n                if (toUpdate.isNotEmpty()) {\n                    SagerDatabase.proxyDao.updateProxy(toUpdate)\n                }\n            } catch (e: IOException) {\n                if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot\n                val profile = DirectBoot.getDeviceProfile()!!\n                profile.tx += outboundStats.uplinkTotal\n                profile.rx += outboundStats.downlinkTotal\n                profile.dirty = true\n                DirectBoot.update(profile)\n                DirectBoot.listenForUnlock()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/SSHInstance.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport io.nekohasekai.sagernet.bg.ClashBasedInstance\nimport io.nekohasekai.sagernet.fmt.ssh.SSHBean\nimport libcore.Libcore\n\nclass SSHInstance(val server: SSHBean, val socksPort: Int) : ClashBasedInstance() {\n\n    override fun createInstance() {\n\n        instance = Libcore.newSSHInstance(\n            socksPort,\n            server.finalAddress,\n            server.finalPort,\n            server.username,\n            server.authType,\n            server.password,\n            server.privateKey,\n            server.privateKeyPassphrase,\n            server.publicKey\n        )\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/ShadowsocksInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport cn.hutool.json.JSONObject\nimport com.github.shadowsocks.plugin.PluginConfiguration\nimport io.nekohasekai.sagernet.bg.ClashBasedInstance\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport libcore.Libcore\n\nclass ShadowsocksInstance(val server: ShadowsocksBean, val port: Int) : ClashBasedInstance() {\n\n    override fun createInstance() {\n        var pluginName = \"\"\n        val pluginOpts = JSONObject()\n\n        if (server.plugin.isNotBlank()) {\n            val plugin = PluginConfiguration(server.plugin)\n            pluginName = plugin.selected\n            val options = plugin.getOptions()\n            when (pluginName) {\n                \"obfs-local\" -> {\n                    pluginOpts[\"mode\"] = options[\"obfs\"]\n                    pluginOpts[\"host\"] = options[\"obfs-host\"]\n                }\n                \"v2ray-plugin\" -> {\n                    pluginOpts[\"mode\"] = options[\"mode\"]\n                    pluginOpts[\"host\"] = options[\"host\"]\n                    pluginOpts[\"path\"] = options[\"path\"]\n\n                    if (options.containsKey(\"tls\")) {\n                        pluginOpts[\"tls\"] = true\n                    }\n                    if (options.containsKey(\"mux\")) {\n                        pluginOpts[\"mux\"] = true\n                    }\n                }\n            }\n        }\n\n        instance = Libcore.newShadowsocksInstance(\n            port,\n            server.finalAddress,\n            server.finalPort,\n            server.password,\n            server.method,\n            pluginName,\n            pluginOpts.toStringPretty()\n        )\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/ShadowsocksRInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport io.nekohasekai.sagernet.bg.ClashBasedInstance\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean\nimport libcore.Libcore\n\nclass ShadowsocksRInstance(val server: ShadowsocksRBean, val port: Int) : ClashBasedInstance() {\n\n    override fun createInstance() {\n        instance = Libcore.newShadowsocksRInstance(\n            port,\n            server.finalAddress,\n            server.finalPort,\n            server.password,\n            server.method,\n            server.obfs,\n            server.obfsParam,\n            server.protocol,\n            server.protocolParam\n        )\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/SnellInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport io.nekohasekai.sagernet.bg.ClashBasedInstance\nimport io.nekohasekai.sagernet.fmt.snell.SnellBean\nimport libcore.Libcore\n\nclass SnellInstance(val server: SnellBean, val port: Int) : ClashBasedInstance() {\n\n    override fun createInstance() {\n\n        instance = Libcore.newSnellInstance(\n            port,\n            server.finalAddress,\n            server.finalPort,\n            server.psk,\n            server.obfsMode,\n            server.obfsHost,\n            server.version\n        )\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/UidDumper.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport android.system.OsConstants\nimport cn.hutool.cache.impl.LFUCacheCompact\nimport cn.hutool.core.util.HexUtil\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport libcore.UidDumper\nimport libcore.UidInfo\nimport java.io.File\nimport java.net.InetAddress\nimport java.net.InetSocketAddress\n\nobject UidDumper : UidDumper {\n\n    private val TCP_IPV4_PROC = File(\"/proc/net/tcp\")\n    private val TCP_IPV6_PROC = File(\"/proc/net/tcp6\")\n    private val UDP_IPV4_PROC = File(\"/proc/net/udp\")\n    private val UDP_IPV6_PROC = File(\"/proc/net/udp6\")\n\n    private data class ProcStats constructor(val remoteAddress: InetSocketAddress, val uid: Int)\n\n    private fun mkMap() = LFUCacheCompact<Int, ProcStats>(-1, 5 * 60 * 1000L).build(false)\n\n    private val uidCacheMapTcp = mkMap()\n    private val uidCacheMapTcp6 = mkMap()\n    private val uidCacheMapUdp = mkMap()\n    private val uidCacheMapUdp6 = mkMap()\n\n    private val canReadProc = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q\n    private val useApi = !canReadProc/* || BuildConfig.DEBUG && tun.enableLog)*/\n\n    override fun dumpUid(\n        ipv6: Boolean, udp: Boolean, srcIp: String, srcPort: Int, destIp: String, destPort: Int\n    ): Int {\n        return dumpUid(\n            ipv6, udp, InetSocketAddress(srcIp, srcPort), InetSocketAddress(destIp, destPort)\n        )\n    }\n\n    override fun getUidInfo(uid: Int): UidInfo {\n        PackageCache.awaitLoadSync()\n\n        if (uid <= 1000L) {\n            val uidInfo = UidInfo()\n            uidInfo.label = PackageCache.loadLabel(\"android\")\n            uidInfo.packageName = \"android\"\n            return uidInfo\n        }\n\n        val packageNames = PackageCache.uidMap[uid.toInt()]\n        if (!packageNames.isNullOrEmpty()) for (packageName in packageNames) {\n            val uidInfo = UidInfo()\n            uidInfo.label = PackageCache.loadLabel(packageName)\n            uidInfo.packageName = packageName\n            return uidInfo\n        }\n\n        error(\"unknown uid $uid\")\n    }\n\n    @SuppressLint(\"NewApi\")\n    fun dumpUid(\n        ipv6: Boolean, udp: Boolean, local: InetSocketAddress, remote: InetSocketAddress\n    ): Int {\n\n        if (useApi) return SagerNet.connectivity.getConnectionOwnerUid(\n            if (!udp) OsConstants.IPPROTO_TCP else OsConstants.IPPROTO_UDP, local, remote\n        )\n\n        val proc = if (!udp) {\n            if (!ipv6) TCP_IPV4_PROC else TCP_IPV6_PROC\n        } else {\n            if (!ipv6) UDP_IPV4_PROC else UDP_IPV6_PROC\n        }\n\n        val cacheMap = if (!udp) {\n            if (!ipv6) uidCacheMapTcp else uidCacheMapTcp6\n        } else {\n            if (!ipv6) uidCacheMapUdp else uidCacheMapUdp6\n        }\n\n        if (cacheMap.containsKey(local.port)) {\n            val cache = cacheMap[local.port]\n            if (cache.remoteAddress == remote) return cache.uid\n        }\n\n        var lines = proc.readLines().map { line ->\n            line.split(\" \").filterNot { it.isBlank() }\n        }\n        lines = lines.subList(1, lines.size)\n\n        for (process in lines) {\n            val localPort = process[1].substringAfter(\":\").toInt(16)\n            val remoteAddress = InetAddress.getByAddress(\n                HexUtil.decodeHex(\n                    process[2].substringBefore(\n                        \":\"\n                    )\n                )\n            )\n            val remotePort = process[2].substringAfter(\":\").toInt(16)\n            val uid = process[7].toInt()\n            cacheMap.put(\n                localPort, ProcStats(InetSocketAddress(remoteAddress, remotePort), uid)\n            )\n        }\n\n        return cacheMap[local.port]?.uid ?: -1\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/proto/V2RayInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.proto\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport android.os.SystemClock\nimport android.webkit.WebResourceError\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.ShadowsocksProvider\nimport io.nekohasekai.sagernet.TrojanProvider\nimport io.nekohasekai.sagernet.bg.AbstractInstance\nimport io.nekohasekai.sagernet.bg.Executable\nimport io.nekohasekai.sagernet.bg.ExternalInstance\nimport io.nekohasekai.sagernet.bg.GuardedProcessPool\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.fmt.V2rayBuildResult\nimport io.nekohasekai.sagernet.fmt.brook.BrookBean\nimport io.nekohasekai.sagernet.fmt.brook.internalUri\nimport io.nekohasekai.sagernet.fmt.buildV2RayConfig\nimport io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean\nimport io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig\nimport io.nekohasekai.sagernet.fmt.internal.ConfigBean\nimport io.nekohasekai.sagernet.fmt.naive.NaiveBean\nimport io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig\nimport io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean\nimport io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean\nimport io.nekohasekai.sagernet.fmt.relaybaton.buildRelayBatonConfig\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.buildShadowsocksConfig\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean\nimport io.nekohasekai.sagernet.fmt.snell.SnellBean\nimport io.nekohasekai.sagernet.fmt.ssh.SSHBean\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean\nimport io.nekohasekai.sagernet.fmt.trojan.buildTrojanConfig\nimport io.nekohasekai.sagernet.fmt.trojan.buildTrojanGoConfig\nimport io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean\nimport io.nekohasekai.sagernet.fmt.trojan_go.buildCustomTrojanConfig\nimport io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig\nimport io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean\nimport io.nekohasekai.sagernet.fmt.wireguard.buildWireGuardUapiConf\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.plugin.PluginManager\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.plus\nimport libcore.Libcore\nimport libcore.V2RayInstance\nimport okhttp3.internal.closeQuietly\nimport java.io.File\nimport java.util.concurrent.atomic.AtomicBoolean\n\nabstract class V2RayInstance(\n    val profile: ProxyEntity\n) : AbstractInstance {\n\n    lateinit var config: V2rayBuildResult\n    lateinit var v2rayPoint: V2RayInstance\n    private lateinit var wsForwarder: WebView\n\n    val pluginPath = hashMapOf<String, PluginManager.InitResult>()\n    val pluginConfigs = hashMapOf<Int, Pair<Int, String>>()\n    val externalInstances = hashMapOf<Int, AbstractInstance>()\n    open lateinit var processes: GuardedProcessPool\n    private var cacheFiles = ArrayList<File>()\n    var closed by AtomicBoolean()\n    fun isInitialized(): Boolean {\n        return ::config.isInitialized\n    }\n\n    protected fun initPlugin(name: String): PluginManager.InitResult {\n        return pluginPath.getOrPut(name) { PluginManager.init(name)!! }\n    }\n\n    protected open fun buildConfig() {\n        config = buildV2RayConfig(profile)\n    }\n\n    protected open fun loadConfig() {\n        v2rayPoint.loadConfig(config.config)\n    }\n\n    open fun init() {\n        v2rayPoint = V2RayInstance()\n        buildConfig()\n        for ((isBalancer, chain) in config.index) {\n            chain.entries.forEachIndexed { index, (port, profile) ->\n                val needChain = !isBalancer && index != chain.size - 1\n                val mux = DataStore.enableMux && (isBalancer || chain.size == 0)\n\n                when (val bean = profile.requireBean()) {\n                    is ShadowsocksBean -> when (val provider = profile.pickShadowsocksProvider()) {\n                        ShadowsocksProvider.CLASH -> {\n                            externalInstances[port] = ShadowsocksInstance(bean, port)\n                        }\n                        else -> {\n                            pluginConfigs[port] = provider to bean.buildShadowsocksConfig(\n                                port\n                            )\n                        }\n                    }\n                    is ShadowsocksRBean -> {\n                        externalInstances[port] = ShadowsocksRInstance(bean, port)\n                    }\n                    is TrojanBean -> {\n                        when (DataStore.providerTrojan) {\n                            TrojanProvider.TROJAN -> {\n                                initPlugin(\"trojan-plugin\")\n                                pluginConfigs[port] = profile.type to bean.buildTrojanConfig(\n                                    port\n                                )\n                            }\n                            TrojanProvider.TROJAN_GO -> {\n                                initPlugin(\"trojan-go-plugin\")\n                                pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(\n                                    port, mux\n                                )\n                            }\n                        }\n                    }\n                    is TrojanGoBean -> {\n                        initPlugin(\"trojan-go-plugin\")\n                        pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(\n                            port, mux\n                        )\n                    }\n                    is NaiveBean -> {\n                        initPlugin(\"naive-plugin\")\n                        pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port, mux)\n                    }\n                    is PingTunnelBean -> {\n                        if (needChain) error(\"PingTunnel is incompatible with chain\")\n                        initPlugin(\"pingtunnel-plugin\")\n                    }\n                    is RelayBatonBean -> {\n                        initPlugin(\"relaybaton-plugin\")\n                        pluginConfigs[port] = profile.type to bean.buildRelayBatonConfig(port)\n                    }\n                    is BrookBean -> {\n                        initPlugin(\"brook-plugin\")\n                    }\n                    is HysteriaBean -> {\n                        initPlugin(\"hysteria-plugin\")\n                        pluginConfigs[port] = profile.type to bean.buildHysteriaConfig(port) {\n                            File(\n                                app.noBackupFilesDir,\n                                \"hysteria_\" + SystemClock.elapsedRealtime() + \".ca\"\n                            ).apply {\n                                parentFile?.mkdirs()\n                                cacheFiles.add(this)\n                            }\n                        }\n                    }\n                    is WireGuardBean -> {\n                        initPlugin(\"wireguard-plugin\")\n                        pluginConfigs[port] = profile.type to bean.buildWireGuardUapiConf()\n                    }\n                    is ConfigBean -> {\n                        when (bean.type) {\n                            \"trojan-go\" -> {\n                                initPlugin(\"trojan-go-plugin\")\n                                pluginConfigs[port] = profile.type to buildCustomTrojanConfig(\n                                    bean.content, port\n                                )\n                            }\n                            else -> {\n                                externalInstances[port] = ExternalInstance(\n                                    profile, port\n                                ).apply {\n                                    init()\n                                }\n                            }\n                        }\n                    }\n                    is SnellBean -> {\n                        externalInstances[port] = SnellInstance(bean, port)\n                    }\n                    is SSHBean -> {\n                        externalInstances[port] = SSHInstance(bean, port)\n                    }\n                }\n            }\n        }\n        loadConfig()\n    }\n\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    override fun launch() {\n        val context = if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked) SagerNet.application else SagerNet.deviceStorage\n\n        for ((isBalancer, chain) in config.index) {\n            chain.entries.forEachIndexed { index, (port, profile) ->\n                val bean = profile.requireBean()\n                val needChain = !isBalancer && index != chain.size - 1\n                val (profileType, config) = pluginConfigs[port] ?: 0 to \"\"\n\n                when {\n                    externalInstances.containsKey(port) -> {\n                        externalInstances[port]!!.launch()\n                    }\n                    bean is ShadowsocksBean -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"shadowsocks_\" + SystemClock.elapsedRealtime() + \".json\"\n                        )\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = mutableListOf(\n                            File(\n                                SagerNet.application.applicationInfo.nativeLibraryDir,\n                                when (profileType) {\n                                    ShadowsocksProvider.SHADOWSOCKS_RUST -> Executable.SS_LOCAL\n                                    else -> Executable.SS_LIBEV_LOCAL\n                                }\n                            ).absolutePath, \"-c\", configFile.absolutePath\n                        )\n\n                        if (profileType == ShadowsocksProvider.SHADOWSOCKS_RUST) {\n                            commands.add(\"--log-without-time\")\n                        } else {\n                            commands.addAll(arrayOf(\"-u\", \"-t\", \"600\"))\n                        }\n\n                        if (DataStore.enableLog) commands.add(\"-v\")\n\n                        processes.start(commands)\n                    }\n                    bean is TrojanBean -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"trojan_\" + SystemClock.elapsedRealtime() + \".json\"\n                        )\n\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = listOf(\n                            when (DataStore.providerTrojan) {\n                                TrojanProvider.TROJAN -> initPlugin(\"trojan-plugin\")\n                                else -> initPlugin(\"trojan-go-plugin\")\n                            }.path, \"--config\", configFile.absolutePath\n                        )\n\n                        processes.start(commands)\n                    }\n                    bean is TrojanGoBean || bean is ConfigBean && bean.type == \"trojan-go\" -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"trojan_go_\" + SystemClock.elapsedRealtime() + \".json\"\n                        )\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = mutableListOf(\n                            initPlugin(\"trojan-go-plugin\").path, \"-config\", configFile.absolutePath\n                        )\n\n                        processes.start(commands)\n                    }\n                    bean is NaiveBean -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"naive_\" + SystemClock.elapsedRealtime() + \".json\"\n                        )\n\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = mutableListOf(\n                            initPlugin(\"naive-plugin\").path, configFile.absolutePath\n                        )\n\n                        processes.start(commands)\n                    }\n                    bean is PingTunnelBean -> {\n                        if (needChain) error(\"PingTunnel is incompatible with chain\")\n\n                        val commands = mutableListOf(\n                            \"su\",\n                            \"-c\",\n                            initPlugin(\"pingtunnel-plugin\").path,\n                            \"-type\",\n                            \"client\",\n                            \"-sock5\",\n                            \"1\",\n                            \"-l\",\n                            \"$LOCALHOST:$port\",\n                            \"-s\",\n                            bean.serverAddress\n                        )\n\n                        if (bean.key.isNotBlank() && bean.key != \"1\") {\n                            commands.add(\"-key\")\n                            commands.add(bean.key)\n                        }\n\n                        processes.start(commands)\n                    }\n                    bean is RelayBatonBean -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"rb_\" + SystemClock.elapsedRealtime() + \".toml\"\n                        )\n\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = mutableListOf(\n                            initPlugin(\"relaybaton-plugin\").path,\n                            \"client\",\n                            \"--config\",\n                            configFile.absolutePath\n                        )\n\n                        processes.start(commands)\n                    }\n                    bean is BrookBean -> {\n                        val commands = mutableListOf(initPlugin(\"brook-plugin\").path)\n\n                        when (bean.protocol) {\n                            \"ws\" -> {\n                                commands.add(\"wsclient\")\n                                commands.add(\"--wsserver\")\n                            }\n                            \"wss\" -> {\n                                commands.add(\"wssclient\")\n                                commands.add(\"--wssserver\")\n                            }\n                            else -> {\n                                commands.add(\"client\")\n                                commands.add(\"--server\")\n                            }\n                        }\n\n                        commands.add(bean.internalUri())\n\n                        if (bean.password.isNotBlank()) {\n                            commands.add(\"--password\")\n                            commands.add(bean.password)\n                        }\n\n                        commands.add(\"--socks5\")\n                        commands.add(\"$LOCALHOST:$port\")\n\n                        processes.start(commands)\n                    }\n                    bean is HysteriaBean -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"hysteria_\" + SystemClock.elapsedRealtime() + \".json\"\n                        )\n\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = mutableListOf(\n                            initPlugin(\"hysteria-plugin\").path,\n                            \"--no-check\",\n                            \"--config\",\n                            configFile.absolutePath,\n                            \"--log-level\",\n                            if (DataStore.enableLog) \"trace\" else \"warn\",\n                            \"client\"\n                        )\n\n                        processes.start(commands)\n                    }\n                    bean is WireGuardBean -> {\n                        val configFile = File(\n                            context.noBackupFilesDir,\n                            \"wg_\" + SystemClock.elapsedRealtime() + \".conf\"\n                        )\n\n                        configFile.parentFile?.mkdirs()\n                        configFile.writeText(config)\n                        cacheFiles.add(configFile)\n\n                        val commands = mutableListOf(\n                            initPlugin(\"wireguard-plugin\").path,\n                            \"-a\",\n                            bean.localAddress.split(\"\\n\").joinToString(\",\"),\n                            \"-b\",\n                            \"127.0.0.1:$port\",\n                            \"-c\",\n                            configFile.absolutePath,\n                            \"-d\",\n                            \"127.0.0.1:${DataStore.localDNSPort}\"\n                        )\n\n                        processes.start(commands)\n                    }\n                }\n            }\n        }\n\n        lateinit var wsUrl: String\n        if (config.requireWs) {\n            val wsPort = mkPort()\n            wsUrl = \"http://$LOCALHOST:$wsPort/\"\n            Libcore.setenv(\"XRAY_BROWSER_DIALER\", \"$LOCALHOST:$wsPort\")\n        } else {\n            Libcore.unsetenv(\"XRAY_BROWSER_DIALER\")\n        }\n\n        v2rayPoint.start()\n\n        if (config.requireWs) {\n\n            runOnMainDispatcher {\n                wsForwarder = WebView(context)\n                wsForwarder.settings.javaScriptEnabled = true\n                wsForwarder.webViewClient = object : WebViewClient() {\n                    override fun onReceivedError(\n                        view: WebView?,\n                        request: WebResourceRequest?,\n                        error: WebResourceError?,\n                    ) {\n                        Logs.d(\"WebView load r: $error\")\n\n                        runOnMainDispatcher {\n                            wsForwarder.loadUrl(\"about:blank\")\n\n                            delay(1000L)\n                            wsForwarder.loadUrl(wsUrl)\n                        }\n                    }\n\n                    override fun onPageFinished(view: WebView, url: String) {\n                        super.onPageFinished(view, url)\n\n                        Logs.d(\"WebView loaded: ${view.title}\")\n\n                    }\n                }\n                wsForwarder.loadUrl(wsUrl)\n            }\n        }\n\n    }\n\n    @Suppress(\"EXPERIMENTAL_API_USAGE\")\n    override fun close() {\n        for (instance in externalInstances.values) {\n            instance.closeQuietly()\n        }\n\n        cacheFiles.removeAll { it.delete(); true }\n\n        if (::wsForwarder.isInitialized) {\n            runBlocking {\n                onMainDispatcher {\n                    wsForwarder.loadUrl(\"about:blank\")\n                    wsForwarder.destroy()\n                }\n            }\n        }\n\n        if (::processes.isInitialized) processes.close(GlobalScope + Dispatchers.IO)\n\n        if (::v2rayPoint.isInitialized) {\n            v2rayPoint.close()\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/test/DebugInstance.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.test\n\n//import libcore.DebugInstance\nimport io.nekohasekai.sagernet.bg.AbstractInstance\n\nclass DebugInstance : AbstractInstance {\n\n//    lateinit var instance: DebugInstance\n\n    override fun launch() {\n//        instance = Libcore.newDebugInstance()\n    }\n\n    override fun close() {\n//        if (::instance.isInitialized) instance.close()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/test/LocalDnsInstance.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.test\n\nimport cn.hutool.core.util.NumberUtil\nimport io.nekohasekai.sagernet.bg.AbstractInstance\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.fmt.TAG_DNS_IN\nimport io.nekohasekai.sagernet.fmt.TAG_DNS_OUT\nimport io.nekohasekai.sagernet.fmt.gson.gson\nimport io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig\nimport io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.*\nimport io.nekohasekai.sagernet.ktx.isIpAddress\nimport libcore.Libcore\nimport libcore.V2RayInstance\nimport java.io.Closeable\n\nclass LocalDnsInstance : AbstractInstance,\n    Closeable {\n\n    lateinit var instance: V2RayInstance\n\n    override fun launch() {\n        val bind = LOCALHOST\n        val directDNS = DataStore.directDns.split(\"\\n\")\n            .mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith(\"#\") } }\n        val config = V2RayConfig().apply {\n            dns = DnsObject().apply {\n                servers = directDNS.map {\n                    DnsObject.StringOrServerObject().apply {\n                        valueY = DnsObject.ServerObject().apply {\n                            address = it\n                        }\n                    }\n                }\n            }\n            inbounds = listOf(InboundObject().apply {\n                tag = TAG_DNS_IN\n                listen = bind\n                port = DataStore.localDNSPort\n                protocol = \"dokodemo-door\"\n                settings = LazyInboundConfigurationObject(this,\n                    DokodemoDoorInboundConfigurationObject().apply {\n                        address = \"1.0.0.1\"\n                        network = \"tcp,udp\"\n                        port = 53\n                    })\n            })\n            outbounds = mutableListOf()\n            outbounds.add(OutboundObject().apply {\n                protocol = \"freedom\"\n                settings = LazyOutboundConfigurationObject(this,\n                    FreedomOutboundConfigurationObject().apply {\n                        domainStrategy = \"UseIP\"\n                    })\n            })\n            outbounds.add(OutboundObject().apply {\n                protocol = \"dns\"\n                tag = TAG_DNS_OUT\n                settings = LazyOutboundConfigurationObject(this,\n                    DNSOutboundConfigurationObject().apply {\n                        var dns = directDNS.first()\n                        if (dns.contains(\":\")) {\n                            val lPort = dns.substringAfterLast(\":\")\n                            dns = dns.substringBeforeLast(\":\")\n                            if (NumberUtil.isInteger(lPort)) {\n                                port = lPort.toInt()\n                            }\n                        }\n                        if (dns.isIpAddress()) {\n                            address = dns\n                        } else if (dns.contains(\"://\")) {\n                            network = \"tcp\"\n                            address = dns.substringAfter(\"://\")\n                        }\n                    })\n            })\n            routing = RoutingObject().apply {\n                domainStrategy = \"AsIs\"\n                rules = listOf(RoutingObject.RuleObject().apply {\n                    type = \"field\"\n                    inboundTag = listOf(TAG_DNS_IN)\n                    outboundTag = TAG_DNS_OUT\n                })\n            }\n        }\n        val i = Libcore.newV2rayInstance()\n        i.loadConfig(gson.toJson(config))\n        i.start()\n\n        instance = i\n    }\n\n    override fun close() {\n        if (::instance.isInitialized) instance.close()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/test/UrlTest.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.test\n\nimport io.nekohasekai.sagernet.bg.proto.SSHInstance\nimport io.nekohasekai.sagernet.bg.proto.ShadowsocksInstance\nimport io.nekohasekai.sagernet.bg.proto.ShadowsocksRInstance\nimport io.nekohasekai.sagernet.bg.proto.SnellInstance\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport libcore.Libcore\n\nclass UrlTest {\n\n    val link = DataStore.connectionTestURL\n    val timeout = 5000\n\n    suspend fun doTest(profile: ProxyEntity): Int {\n        if (profile.useClashBased()) {\n            val instance = when (profile.type) {\n                ProxyEntity.TYPE_SS -> ShadowsocksInstance(profile.ssBean!!, 0)\n                ProxyEntity.TYPE_SSR -> ShadowsocksRInstance(profile.ssrBean!!, 0)\n                ProxyEntity.TYPE_SNELL -> SnellInstance(profile.snellBean!!, 0)\n                ProxyEntity.TYPE_SSH -> SSHInstance(profile.sshBean!!, 0)\n                else -> error(\"unexpected\")\n            }\n            instance.createInstance()\n            return Libcore.urlTestClashBased(instance.instance, link, timeout).toInt()\n        }\n\n        return V2RayTestInstance(profile, link, timeout).doTest()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/bg/test/V2RayTestInstance.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.bg.test\n\nimport io.nekohasekai.sagernet.bg.GuardedProcessPool\nimport io.nekohasekai.sagernet.bg.proto.V2RayInstance\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.fmt.buildV2RayConfig\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.ktx.tryResume\nimport io.nekohasekai.sagernet.ktx.tryResumeWithException\nimport libcore.Libcore\nimport kotlin.coroutines.suspendCoroutine\n\nclass V2RayTestInstance(profile: ProxyEntity, val link: String, val timeout: Int) : V2RayInstance(\n    profile\n) {\n\n    suspend fun doTest(): Int {\n        return suspendCoroutine { c ->\n            processes = GuardedProcessPool {\n                Logs.w(it)\n                c.tryResumeWithException(it)\n            }\n            runOnDefaultDispatcher {\n                use {\n                    try {\n                        init()\n                        launch()\n                        c.tryResume(Libcore.urlTestV2ray(v2rayPoint, \"\", link, timeout))\n                    } catch (e: Exception) {\n                        c.tryResumeWithException(e)\n                    }\n                }\n            }\n        }\n    }\n\n    override fun buildConfig() {\n        config = buildV2RayConfig(profile, true)\n    }\n\n    override fun loadConfig() {\n        v2rayPoint.loadConfig(config.config)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport android.os.Binder\nimport android.os.Build\nimport androidx.preference.PreferenceDataStore\nimport io.nekohasekai.sagernet.*\nimport io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener\nimport io.nekohasekai.sagernet.database.preference.PublicDatabase\nimport io.nekohasekai.sagernet.database.preference.RoomPreferenceDataStore\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.utils.DirectBoot\n\nobject DataStore : OnPreferenceDataStoreChangeListener {\n\n    val configurationStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)\n    val profileCacheStore = RoomPreferenceDataStore(SagerDatabase.profileCacheDao)\n\n    fun init() {\n        if (Build.VERSION.SDK_INT >= 24) {\n            SagerNet.deviceStorage.moveDatabaseFrom(SagerNet.application, Key.DB_PUBLIC)\n        }\n        if (Build.VERSION.SDK_INT >= 24 && directBootAware && SagerNet.user.isUserUnlocked) {\n            DirectBoot.flushTrafficStats()\n        }\n    }\n\n    var selectedProxy by configurationStore.long(Key.PROFILE_ID)\n    var currentProfile by configurationStore.long(Key.PROFILE_CURRENT)\n    var startedProfile by configurationStore.long(Key.PROFILE_STARTED)\n\n    var selectedGroup by configurationStore.long(Key.PROFILE_GROUP) {\n        SagerNet.currentProfile?.groupId ?: 0L\n    }\n\n    fun currentGroupId(): Long {\n        val currentSelected = selectedGroup\n        if (currentSelected > 0L) return currentSelected\n        val groups = SagerDatabase.groupDao.allGroups()\n        if (groups.isNotEmpty()) {\n            val groupId = groups[0].id\n            selectedGroup = groupId\n            return groupId\n        }\n        val groupId = SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true))\n        selectedGroup = groupId\n        return groupId\n    }\n\n    fun currentGroup(): ProxyGroup {\n        var group: ProxyGroup? = null\n        val currentSelected = selectedGroup\n        if (currentSelected > 0L) {\n            group = SagerDatabase.groupDao.getById(currentSelected)\n        }\n        if (group != null) return group\n        val groups = SagerDatabase.groupDao.allGroups()\n        if (groups.isEmpty()) {\n            group = ProxyGroup(ungrouped = true).apply {\n                id = SagerDatabase.groupDao.createGroup(this)\n            }\n        } else {\n            group = groups[0]\n        }\n        selectedGroup = group.id\n        return group\n    }\n\n    fun selectedGroupForImport(): Long {\n        val current = currentGroup()\n        if (current.type == GroupType.BASIC) return current.id\n        val groups = SagerDatabase.groupDao.allGroups()\n        return groups.find { it.type == GroupType.BASIC }!!.id\n    }\n\n    var appTheme by configurationStore.int(Key.APP_THEME)\n    var nightTheme by configurationStore.stringToInt(Key.NIGHT_THEME)\n    var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }\n\n    var domainStrategy by configurationStore.string(Key.DOMAIN_STRATEGY) { \"AsIs\" }\n    var trafficSniffing by configurationStore.boolean(Key.TRAFFIC_SNIFFING) { true }\n    var destinationOverride by configurationStore.boolean(Key.DESTINATION_OVERRIDE)\n    var resolveDestination by configurationStore.boolean(Key.RESOLVE_DESTINATION)\n\n\n    var tcpKeepAliveInterval by configurationStore.stringToInt(Key.TCP_KEEP_ALIVE_INTERVAL) { 15 }\n\n    var bypassLan by configurationStore.boolean(Key.BYPASS_LAN)\n    var bypassLanInCoreOnly by configurationStore.boolean(Key.BYPASS_LAN_IN_CORE_ONLY)\n\n    var allowAccess by configurationStore.boolean(Key.ALLOW_ACCESS)\n    var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL)\n\n    // https://github.com/SagerNet/SagerNet/issues/180\n    var remoteDns by configurationStore.string(Key.REMOTE_DNS) { \"https://1.0.0.1/dns-query\" }\n    var directDns by configurationStore.string(Key.DIRECT_DNS) { \"https+local://223.5.5.5/dns-query\" }\n    var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING)\n    var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS)\n    var hosts by configurationStore.string(Key.DNS_HOSTS) { \"domain:googleapis.cn googleapis.com\" }\n\n    var securityAdvisory by configurationStore.boolean(Key.SECURITY_ADVISORY) { true }\n    var rulesProvider by configurationStore.stringToInt(Key.RULES_PROVIDER)\n    var enableLog by configurationStore.boolean(Key.ENABLE_LOG) { BuildConfig.DEBUG }\n    var enablePcap by configurationStore.boolean(Key.ENABLE_PCAP)\n\n    // hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat\n    private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }\n    var socksPort: Int\n        get() = getLocalPort(Key.SOCKS_PORT, 2081)\n        set(value) = saveLocalPort(Key.SOCKS_PORT, value)\n    var localDNSPort: Int\n        get() = getLocalPort(Key.LOCAL_DNS_PORT, 6451)\n        set(value) {\n            saveLocalPort(Key.LOCAL_DNS_PORT, value)\n        }\n    var httpPort: Int\n        get() = getLocalPort(Key.HTTP_PORT, 9081)\n        set(value) = saveLocalPort(Key.HTTP_PORT, value)\n    var transproxyPort: Int\n        get() = getLocalPort(Key.TRANSPROXY_PORT, 9201)\n        set(value) = saveLocalPort(Key.TRANSPROXY_PORT, value)\n\n    fun initGlobal() {\n        if (configurationStore.getString(Key.SOCKS_PORT) == null) {\n            socksPort = socksPort\n        }\n        if (configurationStore.getString(Key.LOCAL_DNS_PORT) == null) {\n            localDNSPort = localDNSPort\n        }\n        if (configurationStore.getString(Key.HTTP_PORT) == null) {\n            httpPort = httpPort\n        }\n        if (configurationStore.getString(Key.TRANSPROXY_PORT) == null) {\n            transproxyPort = transproxyPort\n        }\n    }\n\n\n    private fun getLocalPort(key: String, default: Int): Int {\n        return parsePort(configurationStore.getString(key), default + userIndex)\n    }\n\n    private fun saveLocalPort(key: String, value: Int) {\n        configurationStore.putString(key, \"$value\")\n    }\n\n    var ipv6Mode by configurationStore.stringToInt(Key.IPV6_MODE) { IPv6Mode.ENABLE }\n\n    var meteredNetwork by configurationStore.boolean(Key.METERED_NETWORK)\n    var proxyApps by configurationStore.boolean(Key.PROXY_APPS)\n    var bypass by configurationStore.boolean(Key.BYPASS_MODE) { true }\n    var individual by configurationStore.string(Key.INDIVIDUAL)\n    var enableMux by configurationStore.boolean(Key.ENABLE_MUX)\n    var enableMuxForAll by configurationStore.boolean(Key.ENABLE_MUX_FOR_ALL)\n    var muxConcurrency by configurationStore.stringToInt(Key.MUX_CONCURRENCY) { 8 }\n    var showStopButton by configurationStore.boolean(Key.SHOW_STOP_BUTTON)\n    var showDirectSpeed by configurationStore.boolean(Key.SHOW_DIRECT_SPEED)\n\n    val persistAcrossReboot by configurationStore.boolean(Key.PERSIST_ACROSS_REBOOT) { true }\n    val canToggleLocked: Boolean get() = configurationStore.getBoolean(Key.DIRECT_BOOT_AWARE) == true\n    val directBootAware: Boolean get() = SagerNet.directBootSupported && canToggleLocked\n\n    var requireHttp by configurationStore.boolean(Key.REQUIRE_HTTP) { true }\n    var appendHttpProxy by configurationStore.boolean(Key.APPEND_HTTP_PROXY) { true }\n    var requireTransproxy by configurationStore.boolean(Key.REQUIRE_TRANSPROXY)\n    var transproxyMode by configurationStore.stringToInt(Key.TRANSPROXY_MODE)\n    var connectionTestURL by configurationStore.string(Key.CONNECTION_TEST_URL) { CONNECTION_TEST_URL }\n    var alwaysShowAddress by configurationStore.boolean(Key.ALWAYS_SHOW_ADDRESS)\n\n    var utlsFingerprint by configurationStore.string(Key.UTLS_FINGERPRINT)\n    var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.GVISOR }\n\n    var appTrafficStatistics by configurationStore.boolean(Key.APP_TRAFFIC_STATISTICS)\n    var profileTrafficStatistics by configurationStore.boolean(Key.PROFILE_TRAFFIC_STATISTICS) { true }\n\n    // protocol\n\n    var providerTrojan by configurationStore.stringToInt(Key.PROVIDER_TROJAN)\n    var providerShadowsocksAEAD by configurationStore.stringToInt(Key.PROVIDER_SS_AEAD)\n    var providerShadowsocksStream by configurationStore.stringToInt(Key.PROVIDER_SS_STREAM)\n\n    // cache\n\n    var dirty by profileCacheStore.boolean(Key.PROFILE_DIRTY)\n    var editingId by profileCacheStore.long(Key.PROFILE_ID)\n    var editingGroup by profileCacheStore.long(Key.PROFILE_GROUP)\n    var profileName by profileCacheStore.string(Key.PROFILE_NAME)\n    var serverAddress by profileCacheStore.string(Key.SERVER_ADDRESS)\n    var serverPort by profileCacheStore.stringToInt(Key.SERVER_PORT)\n    var serverUsername by profileCacheStore.string(Key.SERVER_USERNAME)\n    var serverPassword by profileCacheStore.string(Key.SERVER_PASSWORD)\n    var serverPassword1 by profileCacheStore.string(Key.SERVER_PASSWORD1)\n    var serverMethod by profileCacheStore.string(Key.SERVER_METHOD)\n    var serverPlugin by profileCacheStore.string(Key.SERVER_PLUGIN)\n\n    var serverProtocol by profileCacheStore.string(Key.SERVER_PROTOCOL)\n    var serverProtocolParam by profileCacheStore.string(Key.SERVER_PROTOCOL_PARAM)\n    var serverObfs by profileCacheStore.string(Key.SERVER_OBFS)\n    var serverObfsParam by profileCacheStore.string(Key.SERVER_OBFS_PARAM)\n\n    var serverUserId by profileCacheStore.string(Key.SERVER_USER_ID)\n    var serverAlterId by profileCacheStore.stringToInt(Key.SERVER_ALTER_ID)\n    var serverSecurity by profileCacheStore.string(Key.SERVER_SECURITY)\n    var serverNetwork by profileCacheStore.string(Key.SERVER_NETWORK)\n    var serverHeader by profileCacheStore.string(Key.SERVER_HEADER)\n    var serverHost by profileCacheStore.string(Key.SERVER_HOST)\n    var serverPath by profileCacheStore.string(Key.SERVER_PATH)\n    var serverSNI by profileCacheStore.string(Key.SERVER_SNI)\n    var serverTLS by profileCacheStore.boolean(Key.SERVER_TLS)\n    var serverEncryption by profileCacheStore.string(Key.SERVER_ENCRYPTION)\n    var serverALPN by profileCacheStore.string(Key.SERVER_ALPN)\n    var serverCertificates by profileCacheStore.string(Key.SERVER_CERTIFICATES)\n    var serverFlow by profileCacheStore.string(Key.SERVER_FLOW)\n    var serverQuicSecurity by profileCacheStore.string(Key.SERVER_QUIC_SECURITY)\n    var serverWsBrowserForwarding by profileCacheStore.boolean(Key.SERVER_WS_BROWSER_FORWARDING)\n    var serverHeaders by profileCacheStore.string(Key.SERVER_HEADERS)\n    var serverAllowInsecure by profileCacheStore.boolean(Key.SERVER_ALLOW_INSECURE)\n    var serverMultiMode by profileCacheStore.boolean(Key.SERVER_MULTI_MODE)\n\n    var serverVMessExperimentalAuthenticatedLength by profileCacheStore.boolean(Key.SERVER_VMESS_EXPERIMENTAL_AUTHENTICATED_LENGTH)\n    var serverVMessExperimentalNoTerminationSignal by profileCacheStore.boolean(Key.SERVER_VMESS_EXPERIMENTAL_NO_TERMINATION_SIGNAL)\n\n    var serverAuthType by profileCacheStore.stringToInt(Key.SERVER_AUTH_TYPE)\n    var serverUploadSpeed by profileCacheStore.stringToInt(Key.SERVER_UPLOAD_SPEED)\n    var serverDownloadSpeed by profileCacheStore.stringToInt(Key.SERVER_DOWNLOAD_SPEED)\n    var serverStreamReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_STREAM_RECEIVE_WINDOW)\n    var serverConnectionReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_CONNECTION_RECEIVE_WINDOW)\n    var serverDisableMtuDiscovery by profileCacheStore.boolean(Key.SERVER_DISABLE_MTU_DISCOVERY)\n\n    var serverProtocolVersion by profileCacheStore.stringToInt(Key.SERVER_PROTOCOL)\n    var serverPrivateKey by profileCacheStore.string(Key.SERVER_PRIVATE_KEY)\n    var serverLocalAddress by profileCacheStore.string(Key.SERVER_LOCAL_ADDRESS)\n\n    var balancerType by profileCacheStore.stringToInt(Key.BALANCER_TYPE)\n    var balancerGroup by profileCacheStore.stringToLong(Key.BALANCER_GROUP)\n    var balancerStrategy by profileCacheStore.string(Key.BALANCER_STRATEGY)\n\n    var routeName by profileCacheStore.string(Key.ROUTE_NAME)\n    var routeDomain by profileCacheStore.string(Key.ROUTE_DOMAIN)\n    var routeIP by profileCacheStore.string(Key.ROUTE_IP)\n    var routePort by profileCacheStore.string(Key.ROUTE_PORT)\n    var routeSourcePort by profileCacheStore.string(Key.ROUTE_SOURCE_PORT)\n    var routeNetwork by profileCacheStore.string(Key.ROUTE_NETWORK)\n    var routeSource by profileCacheStore.string(Key.ROUTE_SOURCE)\n    var routeProtocol by profileCacheStore.string(Key.ROUTE_PROTOCOL)\n    var routeAttrs by profileCacheStore.string(Key.ROUTE_ATTRS)\n    var routeOutbound by profileCacheStore.stringToInt(Key.ROUTE_OUTBOUND)\n    var routeOutboundRule by profileCacheStore.long(Key.ROUTE_OUTBOUND_RULE)\n    var routeReverse by profileCacheStore.boolean(Key.ROUTE_REVERSE)\n    var routeRedirect by profileCacheStore.string(Key.ROUTE_REDIRECT)\n    var routePackages by profileCacheStore.string(Key.ROUTE_PACKAGES)\n    var routeForegroundStatus by profileCacheStore.string(Key.ROUTE_FOREGROUND_STATUS)\n\n\n    var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG)\n\n    var groupName by profileCacheStore.string(Key.GROUP_NAME)\n    var groupType by profileCacheStore.stringToInt(Key.GROUP_TYPE)\n    var groupOrder by profileCacheStore.stringToInt(Key.GROUP_ORDER)\n\n    var subscriptionType by profileCacheStore.stringToInt(Key.SUBSCRIPTION_TYPE)\n    var subscriptionLink by profileCacheStore.string(Key.SUBSCRIPTION_LINK)\n    var subscriptionToken by profileCacheStore.string(Key.SUBSCRIPTION_TOKEN)\n    var subscriptionForceResolve by profileCacheStore.boolean(Key.SUBSCRIPTION_FORCE_RESOLVE)\n    var subscriptionDeduplication by profileCacheStore.boolean(Key.SUBSCRIPTION_DEDUPLICATION)\n    var subscriptionForceVMessAEAD by profileCacheStore.boolean(Key.SUBSCRIPTION_FORCE_VMESS_AEAD) { true }\n    var subscriptionUpdateWhenConnectedOnly by profileCacheStore.boolean(Key.SUBSCRIPTION_UPDATE_WHEN_CONNECTED_ONLY)\n    var subscriptionUserAgent by profileCacheStore.string(Key.SUBSCRIPTION_USER_AGENT)\n    var subscriptionAutoUpdate by profileCacheStore.boolean(Key.SUBSCRIPTION_AUTO_UPDATE)\n    var subscriptionAutoUpdateDelay by profileCacheStore.stringToInt(Key.SUBSCRIPTION_AUTO_UPDATE_DELAY) { 360 }\n\n    var rulesFirstCreate by profileCacheStore.boolean(\"rulesFirstCreate\")\n    var systemDnsFinal by profileCacheStore.string(\"systemDnsFinal\")\n\n    override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {\n        when (key) {\n            Key.PROFILE_ID -> if (directBootAware) DirectBoot.update()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport io.nekohasekai.sagernet.GroupType\nimport io.nekohasekai.sagernet.bg.SubscriptionUpdater\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport io.nekohasekai.sagernet.utils.DirectBoot\n\nobject GroupManager {\n\n    interface Listener {\n        suspend fun groupAdd(group: ProxyGroup)\n        suspend fun groupUpdated(group: ProxyGroup)\n\n        suspend fun groupRemoved(groupId: Long)\n        suspend fun groupUpdated(groupId: Long)\n    }\n\n    interface Interface {\n        suspend fun confirm(message: String): Boolean\n        suspend fun alert(message: String)\n        suspend fun onUpdateSuccess(\n            group: ProxyGroup,\n            changed: Int,\n            added: List<String>,\n            updated: Map<String, String>,\n            deleted: List<String>,\n            duplicate: List<String>,\n            byUser: Boolean\n        )\n\n        suspend fun onUpdateFailure(group: ProxyGroup, message: String)\n    }\n\n    private val listeners = ArrayList<Listener>()\n    var userInterface: Interface? = null\n\n    suspend fun iterator(what: suspend Listener.() -> Unit) {\n        synchronized(listeners) {\n            listeners.toList()\n        }.forEach { listener ->\n            what(listener)\n        }\n    }\n\n    fun addListener(listener: Listener) {\n        synchronized(listeners) {\n            listeners.add(listener)\n        }\n    }\n\n    fun removeListener(listener: Listener) {\n        synchronized(listeners) {\n            listeners.remove(listener)\n        }\n    }\n\n    suspend fun clearGroup(groupId: Long) {\n        DataStore.selectedProxy = 0L\n        SagerDatabase.proxyDao.deleteAll(groupId)\n        if (DataStore.directBootAware) DirectBoot.clean()\n        iterator { groupUpdated(groupId) }\n    }\n\n    fun rearrange(groupId: Long) {\n        val entities = SagerDatabase.proxyDao.getByGroup(groupId)\n        for (index in entities.indices) {\n            entities[index].userOrder = (index + 1).toLong()\n        }\n        SagerDatabase.proxyDao.updateProxy(entities)\n    }\n\n    suspend fun postUpdate(group: ProxyGroup) {\n        iterator { groupUpdated(group) }\n    }\n\n    suspend fun postUpdate(groupId: Long) {\n        postUpdate(SagerDatabase.groupDao.getById(groupId) ?: return)\n    }\n\n    suspend fun postReload(groupId: Long) {\n        iterator { groupUpdated(groupId) }\n    }\n\n    suspend fun createGroup(group: ProxyGroup): ProxyGroup {\n        group.userOrder = SagerDatabase.groupDao.nextOrder() ?: 1\n        group.id = SagerDatabase.groupDao.createGroup(group.applyDefaultValues())\n        iterator { groupAdd(group) }\n        if (group.type == GroupType.SUBSCRIPTION) {\n            SubscriptionUpdater.reconfigureUpdater()\n        }\n        return group\n    }\n\n    suspend fun updateGroup(group: ProxyGroup) {\n        SagerDatabase.groupDao.updateGroup(group)\n        iterator { groupUpdated(group) }\n        if (group.type == GroupType.SUBSCRIPTION) {\n            SubscriptionUpdater.reconfigureUpdater()\n        }\n    }\n\n    suspend fun deleteGroup(groupId: Long) {\n        SagerDatabase.groupDao.deleteById(groupId)\n        SagerDatabase.proxyDao.deleteByGroup(groupId)\n        iterator { groupRemoved(groupId) }\n        SubscriptionUpdater.reconfigureUpdater()\n    }\n\n    suspend fun deleteGroup(group: List<ProxyGroup>) {\n        SagerDatabase.groupDao.deleteGroup(group)\n        SagerDatabase.proxyDao.deleteByGroup(group.map { it.id }.toLongArray())\n        for (proxyGroup in group) iterator { groupRemoved(proxyGroup.id) }\n        SubscriptionUpdater.reconfigureUpdater()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport android.database.sqlite.SQLiteCantOpenDatabaseException\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.aidl.TrafficStats\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport io.nekohasekai.sagernet.utils.DirectBoot\nimport java.io.IOException\nimport java.sql.SQLException\nimport java.util.*\n\n\nobject ProfileManager {\n\n    interface Listener {\n        suspend fun onAdd(profile: ProxyEntity)\n        suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats)\n        suspend fun onUpdated(profile: ProxyEntity)\n        suspend fun onRemoved(groupId: Long, profileId: Long)\n    }\n\n    interface RuleListener {\n        suspend fun onAdd(rule: RuleEntity)\n        suspend fun onUpdated(rule: RuleEntity)\n        suspend fun onRemoved(ruleId: Long)\n        suspend fun onCleared()\n    }\n\n    private val listeners = ArrayList<Listener>()\n    private val ruleListeners = ArrayList<RuleListener>()\n\n    suspend fun iterator(what: suspend Listener.() -> Unit) {\n        synchronized(listeners) {\n            listeners.toList()\n        }.forEach { listener ->\n            what(listener)\n        }\n    }\n\n    suspend fun ruleIterator(what: suspend RuleListener.() -> Unit) {\n        val ruleListeners = synchronized(ruleListeners) {\n            ruleListeners.toList()\n        }\n        for (listener in ruleListeners) {\n            what(listener)\n        }\n    }\n\n    fun addListener(listener: Listener) {\n        synchronized(listeners) {\n            listeners.add(listener)\n        }\n    }\n\n    fun removeListener(listener: Listener) {\n        synchronized(listeners) {\n            listeners.remove(listener)\n        }\n    }\n\n    fun addListener(listener: RuleListener) {\n        synchronized(ruleListeners) {\n            ruleListeners.add(listener)\n        }\n    }\n\n    fun removeListener(listener: RuleListener) {\n        synchronized(ruleListeners) {\n            ruleListeners.remove(listener)\n        }\n    }\n\n    suspend fun createProfile(groupId: Long, bean: AbstractBean): ProxyEntity {\n        bean.applyDefaultValues()\n\n        val profile = ProxyEntity(groupId = groupId).apply {\n            id = 0\n            putBean(bean)\n            userOrder = SagerDatabase.proxyDao.nextOrder(groupId) ?: 1\n        }\n        profile.id = SagerDatabase.proxyDao.addProxy(profile)\n        iterator { onAdd(profile) }\n        return profile\n    }\n\n    suspend fun updateProfile(profile: ProxyEntity) {\n        SagerDatabase.proxyDao.updateProxy(profile)\n        iterator { onUpdated(profile) }\n    }\n\n    suspend fun updateProfile(profiles: List<ProxyEntity>) {\n        SagerDatabase.proxyDao.updateProxy(profiles)\n        profiles.forEach {\n            iterator { onUpdated(it) }\n        }\n    }\n\n    suspend fun deleteProfile(groupId: Long, profileId: Long) {\n        if (SagerDatabase.proxyDao.deleteById(profileId) == 0) return\n        if (DataStore.selectedProxy == profileId) {\n            if (DataStore.directBootAware) DirectBoot.clean()\n            DataStore.selectedProxy = 0L\n        }\n        iterator { onRemoved(groupId, profileId) }\n        if (SagerDatabase.proxyDao.countByGroup(groupId) > 1) {\n            GroupManager.rearrange(groupId)\n        }\n    }\n\n    fun getProfile(profileId: Long): ProxyEntity? {\n        if (profileId == 0L) return null\n        return try {\n            SagerDatabase.proxyDao.getById(profileId)\n        } catch (ex: SQLiteCantOpenDatabaseException) {\n            throw IOException(ex)\n        } catch (ex: SQLException) {\n            Logs.w(ex)\n            null\n        }\n    }\n\n    fun getProfiles(profileIds: List<Long>): List<ProxyEntity> {\n        if (profileIds.isEmpty()) return listOf()\n        return try {\n            SagerDatabase.proxyDao.getEntities(profileIds)\n        } catch (ex: SQLiteCantOpenDatabaseException) {\n            throw IOException(ex)\n        } catch (ex: SQLException) {\n            Logs.w(ex)\n            listOf()\n        }\n    }\n\n    suspend fun postUpdate(profileId: Long) {\n        postUpdate(getProfile(profileId) ?: return)\n    }\n\n    suspend fun postUpdate(profile: ProxyEntity) {\n        iterator { onUpdated(profile) }\n    }\n\n    suspend fun postTrafficUpdated(profileId: Long, stats: TrafficStats) {\n        iterator { onUpdated(profileId, stats) }\n    }\n\n    suspend fun createRule(rule: RuleEntity, post: Boolean = true): RuleEntity {\n        rule.userOrder = SagerDatabase.rulesDao.nextOrder() ?: 1\n        rule.id = SagerDatabase.rulesDao.createRule(rule)\n        if (post) {\n            ruleIterator { onAdd(rule) }\n        }\n        return rule\n    }\n\n    suspend fun updateRule(rule: RuleEntity) {\n        SagerDatabase.rulesDao.updateRule(rule)\n        ruleIterator { onUpdated(rule) }\n    }\n\n    suspend fun deleteRule(ruleId: Long) {\n        SagerDatabase.rulesDao.deleteById(ruleId)\n        ruleIterator { onRemoved(ruleId) }\n    }\n\n    suspend fun deleteRules(rules: List<RuleEntity>) {\n        SagerDatabase.rulesDao.deleteRules(rules)\n        ruleIterator {\n            rules.forEach {\n                onRemoved(it.id)\n            }\n        }\n    }\n\n    suspend fun getRules(): List<RuleEntity> {\n        var rules = SagerDatabase.rulesDao.allRules()\n        if (rules.isEmpty() && !DataStore.rulesFirstCreate) {\n            DataStore.rulesFirstCreate = true\n            createRule(\n                RuleEntity(\n                    name = app.getString(R.string.route_opt_block_ads),\n                    domains = \"geosite:category-ads-all\",\n                    outbound = -2\n                )\n            )\n            var country = Locale.getDefault().country.lowercase()\n            var displayCountry = Locale.getDefault().displayCountry\n            if (country in arrayOf(\n                    \"ir\"\n                )\n            ) {\n                createRule(\n                    RuleEntity(\n                        name = app.getString(R.string.route_bypass_domain, displayCountry),\n                        domains = \"domain:$country\",\n                        outbound = -1\n                    ), false\n                )\n            } else {\n                country = Locale.CHINA.country.lowercase()\n                displayCountry = Locale.CHINA.displayCountry\n                createRule(\n                    RuleEntity(\n                        name = app.getString(R.string.route_bypass_domain, displayCountry),\n                        domains = \"geosite:$country\",\n                        outbound = -1\n                    ), false\n                )\n            }\n            createRule(\n                RuleEntity(\n                    name = app.getString(R.string.route_bypass_ip, displayCountry),\n                    ip = \"geoip:$country\",\n                    outbound = -1\n                ), false\n            )\n            rules = SagerDatabase.rulesDao.allRules()\n        }\n        return rules\n    }\n\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Parcel\nimport android.os.Parcelable\nimport androidx.room.*\nimport com.github.shadowsocks.plugin.PluginConfiguration\nimport com.github.shadowsocks.plugin.PluginManager\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ShadowsocksProvider\nimport io.nekohasekai.sagernet.ShadowsocksStreamProvider\nimport io.nekohasekai.sagernet.TrojanProvider\nimport io.nekohasekai.sagernet.aidl.TrafficStats\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.KryoConverters\nimport io.nekohasekai.sagernet.fmt.brook.BrookBean\nimport io.nekohasekai.sagernet.fmt.buildV2RayConfig\nimport io.nekohasekai.sagernet.fmt.http.HttpBean\nimport io.nekohasekai.sagernet.fmt.http.toUri\nimport io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean\nimport io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig\nimport io.nekohasekai.sagernet.fmt.internal.BalancerBean\nimport io.nekohasekai.sagernet.fmt.internal.ChainBean\nimport io.nekohasekai.sagernet.fmt.internal.ConfigBean\nimport io.nekohasekai.sagernet.fmt.naive.NaiveBean\nimport io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig\nimport io.nekohasekai.sagernet.fmt.naive.toUri\nimport io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean\nimport io.nekohasekai.sagernet.fmt.pingtunnel.toUri\nimport io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean\nimport io.nekohasekai.sagernet.fmt.relaybaton.buildRelayBatonConfig\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.buildShadowsocksConfig\nimport io.nekohasekai.sagernet.fmt.shadowsocks.methodsXray\nimport io.nekohasekai.sagernet.fmt.shadowsocks.toUri\nimport io.nekohasekai.sagernet.fmt.shadowsocks.*\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.buildShadowsocksRConfig\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.toUri\nimport io.nekohasekai.sagernet.fmt.snell.SnellBean\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\nimport io.nekohasekai.sagernet.fmt.socks.toUri\nimport io.nekohasekai.sagernet.fmt.ssh.SSHBean\nimport io.nekohasekai.sagernet.fmt.toUniversalLink\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean\nimport io.nekohasekai.sagernet.fmt.trojan.toUri\nimport io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean\nimport io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig\nimport io.nekohasekai.sagernet.fmt.trojan_go.toUri\nimport io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean\nimport io.nekohasekai.sagernet.fmt.v2ray.toUri\nimport io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport io.nekohasekai.sagernet.ktx.ssSecureList\nimport io.nekohasekai.sagernet.ui.profile.*\n\n@Entity(\n    tableName = \"proxy_entities\", indices = [Index(\"groupId\", name = \"groupId\")]\n)\ndata class ProxyEntity(\n    @PrimaryKey(autoGenerate = true) var id: Long = 0L,\n    var groupId: Long = 0L,\n    var type: Int = 0,\n    var userOrder: Long = 0L,\n    var tx: Long = 0L,\n    var rx: Long = 0L,\n    var status: Int = 0,\n    var ping: Int = 0,\n    var uuid: String = \"\",\n    var error: String? = null,\n    var socksBean: SOCKSBean? = null,\n    var httpBean: HttpBean? = null,\n    var ssBean: ShadowsocksBean? = null,\n    var ssrBean: ShadowsocksRBean? = null,\n    var vmessBean: VMessBean? = null,\n    var vlessBean: VLESSBean? = null,\n    var trojanBean: TrojanBean? = null,\n    var trojanGoBean: TrojanGoBean? = null,\n    var naiveBean: NaiveBean? = null,\n    var ptBean: PingTunnelBean? = null,\n    var rbBean: RelayBatonBean? = null,\n    var brookBean: BrookBean? = null,\n    var hysteriaBean: HysteriaBean? = null,\n    var snellBean: SnellBean? = null,\n    var sshBean: SSHBean? = null,\n    var wgBean: WireGuardBean? = null,\n    var configBean: ConfigBean? = null,\n    var chainBean: ChainBean? = null,\n    var balancerBean: BalancerBean? = null\n) : Parcelable {\n\n    companion object {\n        const val TYPE_SOCKS = 0\n        const val TYPE_HTTP = 1\n        const val TYPE_SS = 2\n        const val TYPE_SSR = 3\n        const val TYPE_VMESS = 4\n        const val TYPE_VLESS = 5\n        const val TYPE_TROJAN = 6\n        const val TYPE_TROJAN_GO = 7\n        const val TYPE_NAIVE = 9\n        const val TYPE_PING_TUNNEL = 10\n        const val TYPE_RELAY_BATON = 11\n        const val TYPE_BROOK = 12\n        const val TYPE_HYSTERIA = 15\n        const val TYPE_SNELL = 16\n        const val TYPE_SSH = 17\n        const val TYPE_WG = 18\n\n        const val TYPE_CHAIN = 8\n        const val TYPE_BALANCER = 14\n        const val TYPE_CONFIG = 13\n\n        val chainName by lazy { app.getString(R.string.proxy_chain) }\n        val configName by lazy { app.getString(R.string.custom_config) }\n        val balancerName by lazy { app.getString(R.string.balancer) }\n\n        private val placeHolderBean = SOCKSBean().applyDefaultValues()\n\n        @JvmField\n        val CREATOR = object : Parcelable.Creator<ProxyEntity> {\n            override fun createFromParcel(parcel: Parcel): ProxyEntity {\n                return ProxyEntity(parcel)\n            }\n\n            override fun newArray(size: Int): Array<ProxyEntity?> {\n                return arrayOfNulls(size)\n            }\n        }\n    }\n\n    @Ignore\n    @Transient\n    var dirty: Boolean = false\n\n    @Ignore\n    @Transient\n    var stats: TrafficStats? = null\n\n    constructor(parcel: Parcel) : this(\n        parcel.readLong(),\n        parcel.readLong(),\n        parcel.readInt(),\n        parcel.readLong(),\n        parcel.readLong(),\n        parcel.readLong()\n    ) {\n        dirty = parcel.readByte() > 0\n        val byteArray = ByteArray(parcel.readInt())\n        parcel.readByteArray(byteArray)\n        putByteArray(byteArray)\n    }\n\n    fun putByteArray(byteArray: ByteArray) {\n        when (type) {\n            TYPE_SOCKS -> socksBean = KryoConverters.socksDeserialize(byteArray)\n            TYPE_HTTP -> httpBean = KryoConverters.httpDeserialize(byteArray)\n            TYPE_SS -> ssBean = KryoConverters.shadowsocksDeserialize(byteArray)\n            TYPE_SSR -> ssrBean = KryoConverters.shadowsocksRDeserialize(byteArray)\n            TYPE_VMESS -> vmessBean = KryoConverters.vmessDeserialize(byteArray)\n            TYPE_VLESS -> vlessBean = KryoConverters.vlessDeserialize(byteArray)\n            TYPE_TROJAN -> trojanBean = KryoConverters.trojanDeserialize(byteArray)\n            TYPE_TROJAN_GO -> trojanGoBean = KryoConverters.trojanGoDeserialize(byteArray)\n            TYPE_NAIVE -> naiveBean = KryoConverters.naiveDeserialize(byteArray)\n            TYPE_PING_TUNNEL -> ptBean = KryoConverters.pingTunnelDeserialize(byteArray)\n            TYPE_RELAY_BATON -> rbBean = KryoConverters.relayBatonDeserialize(byteArray)\n            TYPE_BROOK -> brookBean = KryoConverters.brookDeserialize(byteArray)\n            TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray)\n            TYPE_SNELL -> snellBean = KryoConverters.snellDeserialize(byteArray)\n            TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)\n            TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray)\n\n            TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)\n            TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)\n            TYPE_BALANCER -> balancerBean = KryoConverters.balancerBeanDeserialize(byteArray)\n        }\n    }\n\n    override fun writeToParcel(parcel: Parcel, flags: Int) {\n        parcel.writeLong(id)\n        parcel.writeLong(groupId)\n        parcel.writeInt(type)\n        parcel.writeLong(userOrder)\n        parcel.writeLong(tx)\n        parcel.writeLong(rx)\n        parcel.writeByte(if (dirty) 1 else 0)\n        val byteArray = KryoConverters.serialize(requireBean())\n        parcel.writeInt(byteArray.size)\n        parcel.writeByteArray(byteArray)\n    }\n\n    fun displayType() = when (type) {\n        TYPE_SOCKS -> socksBean!!.protocolName()\n        TYPE_HTTP -> if (httpBean!!.tls) \"HTTPS\" else \"HTTP\"\n        TYPE_SS -> \"Shadowsocks\"\n        TYPE_SSR -> \"ShadowsocksR\"\n        TYPE_VMESS -> \"VMess\"\n        TYPE_VLESS -> \"VLESS\"\n        TYPE_TROJAN -> \"Trojan\"\n        TYPE_TROJAN_GO -> \"Trojan-Go\"\n        TYPE_NAIVE -> \"Naïve\"\n        TYPE_PING_TUNNEL -> \"PingTunnel\"\n        TYPE_RELAY_BATON -> \"relaybaton\"\n        TYPE_BROOK -> \"Brook\"\n        TYPE_HYSTERIA -> \"Hysteria\"\n        TYPE_SNELL -> \"Snell\"\n        TYPE_SSH -> \"SSH\"\n        TYPE_WG -> \"WireGuard\"\n        TYPE_CHAIN -> chainName\n        TYPE_CONFIG -> configName\n        TYPE_BALANCER -> balancerName\n        else -> \"Undefined type $type\"\n    }\n\n    fun displayName() = requireBean().displayName()\n    fun displayAddress() = requireBean().displayAddress()\n\n    fun requireBean(): AbstractBean {\n        return when (type) {\n            TYPE_SOCKS -> socksBean\n            TYPE_HTTP -> httpBean\n            TYPE_SS -> ssBean\n            TYPE_SSR -> ssrBean\n            TYPE_VMESS -> vmessBean\n            TYPE_VLESS -> vlessBean\n            TYPE_TROJAN -> trojanBean\n            TYPE_TROJAN_GO -> trojanGoBean\n            TYPE_NAIVE -> naiveBean\n            TYPE_PING_TUNNEL -> ptBean\n            TYPE_RELAY_BATON -> rbBean\n            TYPE_BROOK -> brookBean\n            TYPE_HYSTERIA -> hysteriaBean\n            TYPE_SNELL -> snellBean\n            TYPE_SSH -> sshBean\n            TYPE_WG -> wgBean\n\n            TYPE_CONFIG -> configBean\n            TYPE_CHAIN -> chainBean\n            TYPE_BALANCER -> balancerBean\n            else -> error(\"Undefined type $type\")\n        } ?: error(\"Null ${displayType()} profile\")\n    }\n\n    fun haveLink(): Boolean {\n        return when (type) {\n            TYPE_CHAIN -> false\n            TYPE_CONFIG -> false\n            TYPE_BALANCER -> false\n            else -> true\n        }\n    }\n\n    fun haveStandardLink(): Boolean {\n        return when (requireBean()) {\n            is RelayBatonBean -> false\n            is BrookBean -> false\n            is ConfigBean -> false\n            is HysteriaBean -> false\n            is SnellBean -> false\n            is SSHBean -> false\n            is WireGuardBean -> false\n            else -> true\n        }\n    }\n\n    fun toLink(): String? = with(requireBean()) {\n        when (this) {\n            is SOCKSBean -> toUri()\n            is HttpBean -> toUri()\n            is ShadowsocksBean -> toUri()\n            is ShadowsocksRBean -> toUri()\n            is VMessBean -> toUri()\n            is VLESSBean -> toUri()\n            is TrojanBean -> toUri()\n            is TrojanGoBean -> toUri()\n            is NaiveBean -> toUri()\n            is PingTunnelBean -> toUri()\n            is RelayBatonBean -> toUniversalLink()\n            is BrookBean -> toUniversalLink()\n            is ConfigBean -> toUniversalLink()\n            is HysteriaBean -> toUniversalLink()\n            is SnellBean -> toUniversalLink()\n            is SSHBean -> toUniversalLink()\n            is WireGuardBean -> toUniversalLink()\n            else -> null\n        }\n    }\n\n    fun exportConfig(): Pair<String, String> {\n        var name = \"profile.json\"\n\n        return with(requireBean()) {\n            StringBuilder().apply {\n                val config = buildV2RayConfig(this@ProxyEntity)\n                append(config.config)\n\n                if (!config.index.all { it.chain.isEmpty() }) {\n                    name = \"profiles.txt\"\n                }\n\n                for ((isBalancer, chain) in config.index) {\n                    chain.entries.forEachIndexed { index, (port, profile) ->\n                        val needChain = !isBalancer && index != chain.size - 1\n                        val needMux = index == 0 && DataStore.enableMux\n                        when (val bean = profile.requireBean()) {\n                            is ShadowsocksBean -> {\n                                append(\"\\n\\n\")\n                                append(bean.buildShadowsocksConfig(port))\n\n                            }\n                            is ShadowsocksRBean -> {\n                                append(\"\\n\\n\")\n                                append(bean.buildShadowsocksRConfig())\n                            }\n                            is TrojanGoBean -> {\n                                append(\"\\n\\n\")\n                                append(bean.buildTrojanGoConfig(port, needMux))\n                            }\n                            is NaiveBean -> {\n                                append(\"\\n\\n\")\n                                append(bean.buildNaiveConfig(port, needMux))\n                            }\n                            is RelayBatonBean -> {\n                                append(\"\\n\\n\")\n                                append(bean.buildRelayBatonConfig(port))\n                            }\n                            is HysteriaBean -> {\n                                append(\"\\n\\n\")\n                                append(bean.buildHysteriaConfig(port, null))\n                            }\n                        }\n                    }\n                }\n            }.toString()\n        } to name\n    }\n\n    fun needExternal(): Boolean {\n        return when (type) {\n            TYPE_SOCKS -> false\n            TYPE_HTTP -> false\n            TYPE_SS -> pickShadowsocksProvider() != ShadowsocksProvider.V2RAY\n            TYPE_VMESS -> false\n            TYPE_VLESS -> false\n            TYPE_TROJAN -> DataStore.providerTrojan != TrojanProvider.V2RAY\n            TYPE_CHAIN -> false\n            TYPE_BALANCER -> false\n            else -> true\n        }\n    }\n\n    fun useClashBased(): Boolean {\n        if (!needExternal()) return false\n        return when (type) {\n            TYPE_SS -> pickShadowsocksProvider() == ShadowsocksProvider.CLASH\n            TYPE_SSR -> true\n            TYPE_SNELL -> true\n            TYPE_SSH -> true\n            else -> false\n        }\n    }\n\n    fun isV2RayNetworkTcp(): Boolean {\n        val bean = requireBean() as StandardV2RayBean\n        return when (bean.type) {\n            \"tcp\", \"ws\", \"http\" -> true\n            else -> false\n        }\n    }\n\n    fun needCoreMux(): Boolean {\n        val enableMuxForAll by lazy { DataStore.enableMuxForAll }\n        return when (type) {\n            TYPE_VMESS, TYPE_VLESS -> isV2RayNetworkTcp()\n            TYPE_TROJAN_GO -> false\n            else -> enableMuxForAll\n        }\n    }\n\n    fun pickShadowsocksProvider(): Int {\n        val bean = ssBean ?: return -1\n        if (bean.method.contains(ssSecureList)) {\n            val prefer = DataStore.providerShadowsocksAEAD\n            when {\n                prefer == ShadowsocksProvider.V2RAY && bean.method in methodsXray && bean.plugin.isBlank() -> {\n                    return ShadowsocksProvider.V2RAY\n                }\n                prefer == ShadowsocksProvider.CLASH && bean.method in methodsClash && ssPluginSupportedByClash(\n                    true\n                ) -> {\n                    return ShadowsocksProvider.CLASH\n                }\n                prefer == ShadowsocksProvider.SHADOWSOCKS_RUST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && bean.method in methodsSsRust && !ssPluginSupportedByClash(\n                    false\n                ) -> {\n                    return ShadowsocksProvider.SHADOWSOCKS_RUST\n                }\n                prefer == ShadowsocksProvider.SHADOWSOCKS_LIBEV && bean.method in methodsSsLibev && !ssPluginSupportedByClash(\n                    false\n                ) -> {\n                    return ShadowsocksProvider.SHADOWSOCKS_LIBEV\n                }\n            }\n            return if (ssPreferClash()) {\n                ShadowsocksProvider.CLASH\n            } else if (bean.method in methodsXray && bean.plugin.isBlank()) {\n                ShadowsocksProvider.V2RAY\n            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                ShadowsocksProvider.SHADOWSOCKS_RUST\n            } else {\n                ShadowsocksProvider.SHADOWSOCKS_LIBEV\n            }\n        } else {\n            val prefer = DataStore.providerShadowsocksStream\n            when {\n                prefer == ShadowsocksStreamProvider.CLASH && bean.method in methodsClash && ssPluginSupportedByClash(\n                    true\n                ) -> {\n                    return ShadowsocksProvider.CLASH\n                }\n                prefer == ShadowsocksStreamProvider.SHADOWSOCKS_RUST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && bean.method in methodsSsRust && !ssPluginSupportedByClash(\n                    false\n                ) -> {\n                    return ShadowsocksProvider.SHADOWSOCKS_RUST\n                }\n                prefer == ShadowsocksStreamProvider.SHADOWSOCKS_LIBEV && bean.method in methodsSsLibev && !ssPluginSupportedByClash(\n                    false\n                ) -> {\n                    return ShadowsocksProvider.SHADOWSOCKS_LIBEV\n                }\n            }\n            return if (ssPreferClash()) {\n                ShadowsocksProvider.CLASH\n            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                ShadowsocksProvider.SHADOWSOCKS_RUST\n            } else {\n                ShadowsocksProvider.SHADOWSOCKS_LIBEV\n            }\n        }\n    }\n\n    fun ssPluginSupportedByClash(prefer: Boolean): Boolean {\n        val bean = ssBean ?: return false\n        if (bean.plugin.isNotBlank()) {\n            val plugin = PluginConfiguration(bean.plugin)\n            if (plugin.selected !in arrayOf(\"obfs-local\", \"v2ray-plugin\")) return false\n            if (plugin.selected == \"v2ray-plugin\") {\n                if (plugin.getOptions()[\"mode\"] != \"websocket\") return false\n            }\n            try {\n                PluginManager.init(plugin)\n                return prefer\n            } catch (e: Exception) {\n            }\n            return true\n        }\n        return prefer\n    }\n\n    fun ssPreferClash(): Boolean {\n        val bean = ssBean ?: return false\n        val onlyClash = bean.method !in methodsXray && bean.method !in methodsSsRust && bean.method !in methodsSsLibev\n        return onlyClash || ssPluginSupportedByClash(false)\n    }\n\n    fun putBean(bean: AbstractBean): ProxyEntity {\n        socksBean = null\n        httpBean = null\n        ssBean = null\n        ssrBean = null\n        vmessBean = null\n        vlessBean = null\n        trojanBean = null\n        trojanGoBean = null\n        naiveBean = null\n        ptBean = null\n        rbBean = null\n        brookBean = null\n        hysteriaBean = null\n        snellBean = null\n        sshBean = null\n        wgBean = null\n\n        configBean = null\n        chainBean = null\n        balancerBean = null\n\n        when (bean) {\n            is SOCKSBean -> {\n                type = TYPE_SOCKS\n                socksBean = bean\n            }\n            is HttpBean -> {\n                type = TYPE_HTTP\n                httpBean = bean\n            }\n            is ShadowsocksBean -> {\n                type = TYPE_SS\n                ssBean = bean\n            }\n            is ShadowsocksRBean -> {\n                type = TYPE_SSR\n                ssrBean = bean\n            }\n            is VMessBean -> {\n                type = TYPE_VMESS\n                vmessBean = bean\n            }\n            is VLESSBean -> {\n                type = TYPE_VLESS\n                vlessBean = bean\n            }\n            is TrojanBean -> {\n                type = TYPE_TROJAN\n                trojanBean = bean\n            }\n            is TrojanGoBean -> {\n                type = TYPE_TROJAN_GO\n                trojanGoBean = bean\n            }\n            is NaiveBean -> {\n                type = TYPE_NAIVE\n                naiveBean = bean\n            }\n            is PingTunnelBean -> {\n                type = TYPE_PING_TUNNEL\n                ptBean = bean\n            }\n            is RelayBatonBean -> {\n                type = TYPE_RELAY_BATON\n                rbBean = bean\n            }\n            is BrookBean -> {\n                type = TYPE_BROOK\n                brookBean = bean\n            }\n            is HysteriaBean -> {\n                type = TYPE_HYSTERIA\n                hysteriaBean = bean\n            }\n            is SnellBean -> {\n                type = TYPE_SNELL\n                snellBean = bean\n            }\n            is SSHBean -> {\n                type = TYPE_SSH\n                sshBean = bean\n            }\n            is WireGuardBean -> {\n                type = TYPE_WG\n                wgBean = bean\n            }\n            is ConfigBean -> {\n                type = TYPE_CONFIG\n                configBean = bean\n            }\n            is ChainBean -> {\n                type = TYPE_CHAIN\n                chainBean = bean\n            }\n            is BalancerBean -> {\n                type = TYPE_BALANCER\n                balancerBean = bean\n            }\n            else -> error(\"Undefined type $type\")\n        }\n        return this\n    }\n\n    fun settingIntent(ctx: Context, isSubscription: Boolean): Intent {\n        return Intent(\n            ctx, when (type) {\n                TYPE_SOCKS -> SocksSettingsActivity::class.java\n                TYPE_HTTP -> HttpSettingsActivity::class.java\n                TYPE_SS -> ShadowsocksSettingsActivity::class.java\n                TYPE_SSR -> ShadowsocksRSettingsActivity::class.java\n                TYPE_VMESS -> VMessSettingsActivity::class.java\n                TYPE_VLESS -> VLESSSettingsActivity::class.java\n                TYPE_TROJAN -> TrojanSettingsActivity::class.java\n                TYPE_TROJAN_GO -> TrojanGoSettingsActivity::class.java\n                TYPE_NAIVE -> NaiveSettingsActivity::class.java\n                TYPE_PING_TUNNEL -> PingTunnelSettingsActivity::class.java\n                TYPE_RELAY_BATON -> RelayBatonSettingsActivity::class.java\n                TYPE_BROOK -> BrookSettingsActivity::class.java\n                TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java\n                TYPE_SNELL -> SnellSettingsActivity::class.java\n                TYPE_SSH -> SSHSettingsActivity::class.java\n                TYPE_WG -> WireGuardSettingsActivity::class.java\n\n                TYPE_CONFIG -> ConfigSettingsActivity::class.java\n                TYPE_CHAIN -> ChainSettingsActivity::class.java\n                TYPE_BALANCER -> BalancerSettingsActivity::class.java\n                else -> throw IllegalArgumentException()\n            }\n        ).apply {\n            putExtra(ProfileSettingsActivity.EXTRA_PROFILE_ID, id)\n            putExtra(ProfileSettingsActivity.EXTRA_IS_SUBSCRIPTION, isSubscription)\n        }\n    }\n\n    @androidx.room.Dao\n    interface Dao {\n\n        @Query(\"SELECT id FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder\")\n        fun getIdsByGroup(groupId: Long): List<Long>\n\n        @Query(\"SELECT * FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder\")\n        fun getByGroup(groupId: Long): List<ProxyEntity>\n\n        @Query(\"SELECT * FROM proxy_entities WHERE id in (:proxyIds)\")\n        fun getEntities(proxyIds: List<Long>): List<ProxyEntity>\n\n        @Query(\"SELECT COUNT(*) FROM proxy_entities WHERE groupId = :groupId\")\n        fun countByGroup(groupId: Long): Long\n\n        @Query(\"SELECT  MAX(userOrder) + 1 FROM proxy_entities WHERE groupId = :groupId\")\n        fun nextOrder(groupId: Long): Long?\n\n        @Query(\"SELECT * FROM proxy_entities WHERE id = :proxyId\")\n        fun getById(proxyId: Long): ProxyEntity?\n\n        @Query(\"DELETE FROM proxy_entities WHERE id IN (:proxyId)\")\n        fun deleteById(proxyId: Long): Int\n\n        @Query(\"DELETE FROM proxy_entities WHERE groupId = :groupId\")\n        fun deleteByGroup(groupId: Long)\n\n        @Query(\"DELETE FROM proxy_entities WHERE groupId in (:groupId)\")\n        fun deleteByGroup(groupId: LongArray)\n\n        @Delete\n        fun deleteProxy(proxy: ProxyEntity): Int\n\n        @Delete\n        fun deleteProxy(proxies: List<ProxyEntity>): Int\n\n        @Update\n        fun updateProxy(proxy: ProxyEntity): Int\n\n        @Update\n        fun updateProxy(proxies: List<ProxyEntity>): Int\n\n        @Insert\n        fun addProxy(proxy: ProxyEntity): Long\n\n        @Query(\"DELETE FROM proxy_entities WHERE groupId = :groupId\")\n        fun deleteAll(groupId: Long): Int\n\n    }\n\n    override fun describeContents(): Int {\n        return 0\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport androidx.room.*\nimport com.esotericsoftware.kryo.io.ByteBufferInput\nimport com.esotericsoftware.kryo.io.ByteBufferOutput\nimport io.nekohasekai.sagernet.GroupOrder\nimport io.nekohasekai.sagernet.GroupType\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.fmt.Serializable\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\n\n@Entity(tableName = \"proxy_groups\")\ndata class ProxyGroup(\n    @PrimaryKey(autoGenerate = true) var id: Long = 0L,\n    var userOrder: Long = 0L,\n    var ungrouped: Boolean = false,\n    var name: String? = null,\n    var type: Int = GroupType.BASIC,\n    var subscription: SubscriptionBean? = null,\n    var order: Int = GroupOrder.ORIGIN,\n) : Serializable() {\n\n    @Transient\n    var export = false\n\n    override fun initializeDefaultValues() {\n        subscription?.applyDefaultValues()\n    }\n\n    override fun serializeToBuffer(output: ByteBufferOutput) {\n        if (export) {\n\n            output.writeInt(0)\n            output.writeString(name)\n            output.writeInt(type)\n            val subscription = subscription!!\n            subscription.serializeForShare(output)\n\n        } else {\n            output.writeInt(0)\n            output.writeLong(id)\n            output.writeLong(userOrder)\n            output.writeBoolean(ungrouped)\n            output.writeString(name)\n            output.writeInt(type)\n\n            if (type == GroupType.SUBSCRIPTION) {\n                subscription?.serializeToBuffer(output)\n            }\n        }\n    }\n\n    override fun deserializeFromBuffer(input: ByteBufferInput) {\n        if (export) {\n            val version = input.readInt()\n\n            name = input.readString()\n            type = input.readInt()\n            val subscription = SubscriptionBean()\n            this.subscription = subscription\n\n            subscription.deserializeFromShare(input)\n        } else {\n            val version = input.readInt()\n\n            id = input.readLong()\n            userOrder = input.readLong()\n            ungrouped = input.readBoolean()\n            name = input.readString()\n            type = input.readInt()\n\n            if (type == GroupType.SUBSCRIPTION) {\n                val subscription = SubscriptionBean()\n                this.subscription = subscription\n\n                subscription.deserializeFromBuffer(input)\n            }\n        }\n    }\n\n    fun displayName(): String {\n        return name.takeIf { !it.isNullOrBlank() } ?: app.getString(R.string.group_default)\n    }\n\n    @androidx.room.Dao\n    interface Dao {\n\n        @Query(\"SELECT * FROM proxy_groups ORDER BY userOrder\")\n        fun allGroups(): List<ProxyGroup>\n\n        @Query(\"SELECT * FROM proxy_groups WHERE type = ${GroupType.SUBSCRIPTION}\")\n        suspend fun subscriptions(): List<ProxyGroup>\n\n        @Query(\"SELECT MAX(userOrder) + 1 FROM proxy_groups\")\n        fun nextOrder(): Long?\n\n        @Query(\"SELECT * FROM proxy_groups WHERE id = :groupId\")\n        fun getById(groupId: Long): ProxyGroup?\n\n        @Query(\"DELETE FROM proxy_groups WHERE id = :groupId\")\n        fun deleteById(groupId: Long): Int\n\n        @Delete\n        fun deleteGroup(group: ProxyGroup)\n\n        @Delete\n        fun deleteGroup(groupList: List<ProxyGroup>)\n\n        @Insert\n        fun createGroup(group: ProxyGroup): Long\n\n        @Update\n        fun updateGroup(group: ProxyGroup)\n\n    }\n\n    companion object CREATOR : Serializable.CREATOR<ProxyGroup>() {\n\n        override fun newInstance(): ProxyGroup {\n            return ProxyGroup()\n        }\n\n        override fun newArray(size: Int): Array<ProxyGroup?> {\n            return arrayOfNulls(size)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport android.os.Parcelable\nimport androidx.room.*\nimport io.nekohasekai.sagernet.AppStatus\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ktx.app\nimport kotlinx.parcelize.Parcelize\n\n@Entity(tableName = \"rules\")\n@Parcelize\ndata class RuleEntity(\n    @PrimaryKey(autoGenerate = true) var id: Long = 0L,\n    var name: String = \"\",\n    var userOrder: Long = 0L,\n    var enabled: Boolean = false,\n    var domains: String = \"\",\n    var ip: String = \"\",\n    var port: String = \"\",\n    var sourcePort: String = \"\",\n    var network: String = \"\",\n    var source: String = \"\",\n    var protocol: String = \"\",\n    var attrs: String = \"\",\n    var outbound: Long = 0,\n    var reverse: Boolean = false,\n    var redirect: String = \"\",\n    var packages: List<String> = listOf(),\n    var appStatus: List<String> = listOf(),\n) : Parcelable {\n\n    fun isBypassRule(): Boolean {\n        return (domains.isNotBlank() && ip.isBlank() || ip.isNotBlank() && domains.isBlank()) && port.isBlank() && sourcePort.isBlank() && network.isBlank() && source.isBlank() && protocol.isBlank() && attrs.isBlank() && !reverse && redirect.isBlank() && outbound == -1L && packages.isEmpty() && appStatus.isEmpty()\n    }\n\n    fun displayName(): String {\n        return name.takeIf { it.isNotBlank() } ?: \"Rule $id\"\n    }\n\n    fun mkSummary(): String {\n        var summary = \"\"\n        if (domains.isNotBlank()) summary += \"$domains\\n\"\n        if (ip.isNotBlank()) summary += \"$ip\\n\"\n        if (sourcePort.isNotBlank()) summary += \"$sourcePort\\n\"\n        if (network.isNotBlank()) summary += \"$network\\n\"\n        if (source.isNotBlank()) summary += \"$source\\n\"\n        if (protocol.isNotBlank()) summary += \"$protocol\\n\"\n        if (attrs.isNotBlank()) summary += \"$attrs\\n\"\n        if (reverse) summary += \"$redirect\\n\"\n        if (packages.isNotEmpty()) summary += app.getString(\n            R.string.apps_message, packages.size\n        ) + \"\\n\"\n        if (appStatus.isNotEmpty()) summary += displayAppStatus().joinToString(\"\\n\")\n        val lines = summary.trim().split(\"\\n\")\n        return if (lines.size > 3) {\n            lines.subList(0, 3).joinToString(\"\\n\", postfix = \"\\n...\")\n        } else {\n            summary.trim()\n        }\n    }\n\n    fun displayOutbound(): String {\n        if (reverse) {\n            return app.getString(R.string.route_reverse)\n        }\n        return when (outbound) {\n            0L -> app.getString(R.string.route_proxy)\n            -1L -> app.getString(R.string.route_bypass)\n            -2L -> app.getString(R.string.route_block)\n            else -> ProfileManager.getProfile(outbound)?.displayName()\n                ?: app.getString(R.string.route_proxy)\n        }\n    }\n\n    fun displayAppStatus(): List<String> {\n        return appStatus.map {\n            when (it) {\n                AppStatus.FOREGROUND -> app.getString(R.string.foreground)\n                /*AppStatus.BACKGROUND*/ else -> app.getString(R.string.background)\n            }\n        }\n    }\n\n    @androidx.room.Dao\n    interface Dao {\n\n        @Query(\"SELECT * from rules WHERE (appStatus != '' OR packages != '') AND enabled = 1\")\n        fun checkVpnNeeded(): List<RuleEntity>\n\n        @Query(\"SELECT * FROM rules ORDER BY userOrder\")\n        fun allRules(): List<RuleEntity>\n\n        @Query(\"SELECT * FROM rules WHERE enabled = :enabled ORDER BY userOrder\")\n        fun enabledRules(enabled: Boolean = true): List<RuleEntity>\n\n        @Query(\"SELECT MAX(userOrder) + 1 FROM rules\")\n        fun nextOrder(): Long?\n\n        @Query(\"SELECT * FROM rules WHERE id = :ruleId\")\n        fun getById(ruleId: Long): RuleEntity?\n\n        @Query(\"DELETE FROM rules WHERE id = :ruleId\")\n        fun deleteById(ruleId: Long): Int\n\n        @Delete\n        fun deleteRule(rule: RuleEntity)\n\n        @Delete\n        fun deleteRules(rules: List<RuleEntity>)\n\n        @Insert\n        fun createRule(rule: RuleEntity): Long\n\n        @Update\n        fun updateRule(rule: RuleEntity)\n\n        @Update\n        fun updateRules(rules: List<RuleEntity>)\n\n        @Query(\"DELETE FROM rules\")\n        fun deleteAll()\n\n    }\n\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport androidx.room.Database\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport androidx.room.TypeConverters\nimport dev.matrix.roomigrant.GenerateRoomMigrations\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.preference.KeyValuePair\nimport io.nekohasekai.sagernet.fmt.KryoConverters\nimport io.nekohasekai.sagernet.fmt.gson.GsonConverters\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\n\n@Database(\n    entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class, StatsEntity::class, KeyValuePair::class],\n    version = 10\n)\n@TypeConverters(value = [KryoConverters::class, GsonConverters::class])\n@GenerateRoomMigrations\nabstract class SagerDatabase : RoomDatabase() {\n\n    companion object {\n        @Suppress(\"EXPERIMENTAL_API_USAGE\")\n        private val instance by lazy {\n            SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs()\n            Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE)\n                .addMigrations(*SagerDatabase_Migrations.build())\n                .allowMainThreadQueries()\n                .enableMultiInstanceInvalidation()\n                .fallbackToDestructiveMigration()\n                .setQueryExecutor { GlobalScope.launch { it.run() } }\n                .build()\n        }\n\n        val profileCacheDao get() = instance.profileCacheDao()\n        val groupDao get() = instance.groupDao()\n        val proxyDao get() = instance.proxyDao()\n        val rulesDao get() = instance.rulesDao()\n        val statsDao get() = instance.statsDao()\n\n    }\n\n    abstract fun profileCacheDao(): KeyValuePair.Dao\n    abstract fun groupDao(): ProxyGroup.Dao\n    abstract fun proxyDao(): ProxyEntity.Dao\n    abstract fun rulesDao(): RuleEntity.Dao\n    abstract fun statsDao(): StatsEntity.Dao\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/StatsEntity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database\n\nimport android.os.Parcelable\nimport androidx.room.*\nimport io.nekohasekai.sagernet.aidl.AppStats\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport kotlinx.parcelize.Parcelize\n\n@Entity(\n    tableName = \"stats\", indices = [Index(\n        \"packageName\", unique = true\n    )]\n)\n@Parcelize\nclass StatsEntity(\n    @PrimaryKey(autoGenerate = true) var id: Int = 0,\n    var packageName: String = \"\",\n    var tcpConnections: Int = 0,\n    var udpConnections: Int = 0,\n    var uplink: Long = 0L,\n    var downlink: Long = 0L\n) : Parcelable {\n\n    fun toStats(): AppStats {\n        return AppStats(\n            packageName,\n            PackageCache[packageName] ?: 1000,\n            0,\n            0,\n            tcpConnections,\n            udpConnections,\n            0,\n            0,\n            uplink,\n            downlink,\n            0\n        )\n    }\n\n    @androidx.room.Dao\n    interface Dao {\n\n        @Query(\"SELECT * FROM stats\")\n        fun all(): List<StatsEntity>\n\n        @Query(\"SELECT * FROM stats WHERE packageName = :packageName\")\n        operator fun get(packageName: String): StatsEntity?\n\n        @Query(\"DELETE FROM stats WHERE packageName = :packageName\")\n        fun delete(packageName: String): Int\n\n        @Insert\n        fun create(stats: StatsEntity)\n\n        @Update\n        fun update(stats: List<StatsEntity>)\n\n        @Query(\"DELETE FROM stats\")\n        fun deleteAll()\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/SubscriptionBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport io.nekohasekai.sagernet.SubscriptionType;\nimport io.nekohasekai.sagernet.fmt.Serializable;\nimport io.nekohasekai.sagernet.ktx.KryosKt;\n\npublic class SubscriptionBean extends Serializable {\n\n    public Integer type;\n    public String link;\n    public String token;\n    public Boolean forceResolve;\n    public Boolean deduplication;\n    public Boolean forceVMessAEAD;\n    public Boolean updateWhenConnectedOnly;\n    public String customUserAgent;\n    public Boolean autoUpdate;\n    public Integer autoUpdateDelay;\n    public Integer lastUpdated;\n\n    // SIP008\n\n    public Long bytesUsed;\n    public Long bytesRemaining;\n\n    // Open Online Config\n\n    public String username;\n    public Integer expiryDate;\n    public List<String> protocols;\n\n    public Set<String> selectedGroups;\n    public Set<String> selectedOwners;\n    public Set<String> selectedTags;\n\n    public SubscriptionBean() {\n    }\n\n    @Override\n    public void serializeToBuffer(ByteBufferOutput output) {\n        output.writeInt(1);\n\n        output.writeInt(type);\n\n        if (type == SubscriptionType.OOCv1) {\n            output.writeString(token);\n        } else {\n            output.writeString(link);\n        }\n\n        output.writeBoolean(forceResolve);\n        output.writeBoolean(deduplication);\n        output.writeBoolean(forceVMessAEAD);\n        output.writeBoolean(updateWhenConnectedOnly);\n        output.writeString(customUserAgent);\n        output.writeBoolean(autoUpdate);\n        output.writeInt(autoUpdateDelay);\n        output.writeInt(lastUpdated);\n\n        if (type != SubscriptionType.RAW) {\n            output.writeLong(bytesUsed);\n            output.writeLong(bytesRemaining);\n        }\n\n        if (type == SubscriptionType.OOCv1) {\n            output.writeString(username);\n            output.writeInt(expiryDate);\n            KryosKt.writeStringList(output, protocols);\n            KryosKt.writeStringList(output, selectedGroups);\n            KryosKt.writeStringList(output, selectedOwners);\n            KryosKt.writeStringList(output, selectedTags);\n        }\n\n    }\n\n    public void serializeForShare(ByteBufferOutput output) {\n        output.writeInt(0);\n\n        output.writeInt(type);\n\n        if (type == SubscriptionType.OOCv1) {\n            output.writeString(token);\n        } else {\n            output.writeString(link);\n        }\n\n        output.writeBoolean(forceResolve);\n        output.writeBoolean(deduplication);\n        output.writeBoolean(forceVMessAEAD);\n        output.writeBoolean(updateWhenConnectedOnly);\n        output.writeString(customUserAgent);\n\n        if (type != SubscriptionType.RAW) {\n            output.writeLong(bytesUsed);\n            output.writeLong(bytesRemaining);\n        }\n\n        if (type == SubscriptionType.OOCv1) {\n            output.writeString(username);\n            output.writeInt(expiryDate);\n            KryosKt.writeStringList(output, protocols);\n        }\n\n    }\n\n    @Override\n    public void deserializeFromBuffer(ByteBufferInput input) {\n        int version = input.readInt();\n\n        type = input.readInt();\n        if (type == SubscriptionType.OOCv1) {\n            token = input.readString();\n        } else {\n            link = input.readString();\n        }\n        forceResolve = input.readBoolean();\n        deduplication = input.readBoolean();\n        forceVMessAEAD = input.readBoolean();\n        updateWhenConnectedOnly = input.readBoolean();\n        customUserAgent = input.readString();\n        autoUpdate = input.readBoolean();\n        autoUpdateDelay = input.readInt();\n        lastUpdated = input.readInt();\n\n        if (type != SubscriptionType.RAW) {\n            bytesUsed = input.readLong();\n            bytesRemaining = input.readLong();\n        }\n\n        if (type == SubscriptionType.OOCv1) {\n            username = input.readString();\n            expiryDate = input.readInt();\n            protocols = KryosKt.readStringList(input);\n            if (input.canReadVarInt()) {\n                selectedGroups = KryosKt.readStringSet(input);\n                if (version >= 1) {\n                    selectedOwners = KryosKt.readStringSet(input);\n                }\n                selectedTags = KryosKt.readStringSet(input);\n            }\n        }\n    }\n\n    public void deserializeFromShare(ByteBufferInput input) {\n        int version = input.readInt();\n\n        type = input.readInt();\n        if (type == SubscriptionType.OOCv1) {\n            token = input.readString();\n        } else {\n            link = input.readString();\n        }\n        forceResolve = input.readBoolean();\n        deduplication = input.readBoolean();\n        forceVMessAEAD = input.readBoolean();\n        updateWhenConnectedOnly = input.readBoolean();\n        customUserAgent = input.readString();\n\n        if (type != SubscriptionType.RAW) {\n            bytesUsed = input.readLong();\n            bytesRemaining = input.readLong();\n        }\n\n        if (type == SubscriptionType.OOCv1) {\n            username = input.readString();\n            expiryDate = input.readInt();\n            protocols = KryosKt.readStringList(input);\n        }\n    }\n\n    @Override\n    public void initializeDefaultValues() {\n        if (type == null) type = SubscriptionType.RAW;\n        if (link == null) link = \"\";\n        if (token == null) token = \"\";\n        if (forceResolve == null) forceResolve = false;\n        if (deduplication == null) deduplication = false;\n        if (forceVMessAEAD == null) forceVMessAEAD = false;\n        if (updateWhenConnectedOnly == null) updateWhenConnectedOnly = false;\n        if (customUserAgent == null) customUserAgent = \"\";\n        if (autoUpdate == null) autoUpdate = false;\n        if (autoUpdateDelay == null) autoUpdateDelay = 1440;\n        if (lastUpdated == null) lastUpdated = 0;\n\n        if (bytesUsed == null) bytesUsed = 0L;\n        if (bytesRemaining == null) bytesRemaining = 0L;\n\n        if (username == null) username = \"\";\n        if (expiryDate == null) expiryDate = 0;\n        if (protocols == null) protocols = new ArrayList<>();\n        if (selectedGroups == null) selectedGroups = new LinkedHashSet<>();\n        if (selectedOwners == null) selectedOwners = new LinkedHashSet<>();\n        if (selectedTags == null) selectedTags = new LinkedHashSet<>();\n\n    }\n\n    public static final Creator<SubscriptionBean> CREATOR = new CREATOR<SubscriptionBean>() {\n        @NonNull\n        @Override\n        public SubscriptionBean newInstance() {\n            return new SubscriptionBean();\n        }\n\n        @Override\n        public SubscriptionBean[] newArray(int size) {\n            return new SubscriptionBean[size];\n        }\n    };\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/preference/EditTextPreferenceModifiers.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database.preference\n\nimport android.graphics.Typeface\nimport android.text.InputFilter\nimport android.view.inputmethod.EditorInfo\nimport android.widget.EditText\nimport androidx.preference.EditTextPreference\n\nobject EditTextPreferenceModifiers {\n    object Monospace : EditTextPreference.OnBindEditTextListener {\n        override fun onBindEditText(editText: EditText) {\n            editText.typeface = Typeface.MONOSPACE\n        }\n    }\n\n    object Hosts : EditTextPreference.OnBindEditTextListener {\n\n        override fun onBindEditText(editText: EditText) {\n            editText.setHorizontallyScrolling(true)\n            editText.setSelection(editText.text.length)\n        }\n    }\n\n    object Port : EditTextPreference.OnBindEditTextListener {\n        private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))\n\n        override fun onBindEditText(editText: EditText) {\n            editText.inputType = EditorInfo.TYPE_CLASS_NUMBER\n            editText.filters = portLengthFilter\n            editText.setSingleLine()\n            editText.setSelection(editText.text.length)\n        }\n    }\n\n    object Number : EditTextPreference.OnBindEditTextListener {\n\n        override fun onBindEditText(editText: EditText) {\n            editText.inputType = EditorInfo.TYPE_CLASS_NUMBER\n            editText.setSingleLine()\n            editText.setSelection(editText.text.length)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/preference/KeyValuePair.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database.preference\n\nimport androidx.room.*\nimport java.io.ByteArrayOutputStream\nimport java.nio.ByteBuffer\n\n@Entity\nclass KeyValuePair() {\n    companion object {\n        const val TYPE_UNINITIALIZED = 0\n        const val TYPE_BOOLEAN = 1\n        const val TYPE_FLOAT = 2\n\n        @Deprecated(\"Use TYPE_LONG.\")\n        const val TYPE_INT = 3\n        const val TYPE_LONG = 4\n        const val TYPE_STRING = 5\n        const val TYPE_STRING_SET = 6\n    }\n\n    @androidx.room.Dao\n    interface Dao {\n\n        @Query(\"SELECT * FROM `KeyValuePair`\")\n        fun all(): List<KeyValuePair>\n\n        @Query(\"DELETE FROM `KeyValuePair`\")\n        fun deleteAll(): Int\n\n        @Query(\"SELECT * FROM `KeyValuePair` WHERE `key` = :key\")\n        operator fun get(key: String): KeyValuePair?\n\n        @Insert(onConflict = OnConflictStrategy.REPLACE)\n        fun put(value: KeyValuePair): Long\n\n        @Query(\"DELETE FROM `KeyValuePair` WHERE `key` = :key\")\n        fun delete(key: String): Int\n    }\n\n    @PrimaryKey\n    var key: String = \"\"\n    var valueType: Int = TYPE_UNINITIALIZED\n    var value: ByteArray = ByteArray(0)\n\n    val boolean: Boolean?\n        get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null\n    val float: Float?\n        get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null\n\n    @Suppress(\"DEPRECATION\")\n    @Deprecated(\"Use long.\", ReplaceWith(\"long\"))\n    val int: Int?\n        get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null\n    val long: Long?\n        get() = when (valueType) {\n            @Suppress(\"DEPRECATION\") TYPE_INT,\n            -> ByteBuffer.wrap(value).int.toLong()\n            TYPE_LONG -> ByteBuffer.wrap(value).long\n            else -> null\n        }\n    val string: String?\n        get() = if (valueType == TYPE_STRING) String(value) else null\n    val stringSet: Set<String>?\n        get() = if (valueType == TYPE_STRING_SET) {\n            val buffer = ByteBuffer.wrap(value)\n            val result = HashSet<String>()\n            while (buffer.hasRemaining()) {\n                val chArr = ByteArray(buffer.int)\n                buffer.get(chArr)\n                result.add(String(chArr))\n            }\n            result\n        } else null\n\n    @Ignore\n    constructor(key: String) : this() {\n        this.key = key\n    }\n\n    // putting null requires using DataStore\n    fun put(value: Boolean): KeyValuePair {\n        valueType = TYPE_BOOLEAN\n        this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()\n        return this\n    }\n\n    fun put(value: Float): KeyValuePair {\n        valueType = TYPE_FLOAT\n        this.value = ByteBuffer.allocate(4).putFloat(value).array()\n        return this\n    }\n\n    @Suppress(\"DEPRECATION\")\n    @Deprecated(\"Use long.\")\n    fun put(value: Int): KeyValuePair {\n        valueType = TYPE_INT\n        this.value = ByteBuffer.allocate(4).putInt(value).array()\n        return this\n    }\n\n    fun put(value: Long): KeyValuePair {\n        valueType = TYPE_LONG\n        this.value = ByteBuffer.allocate(8).putLong(value).array()\n        return this\n    }\n\n    fun put(value: String): KeyValuePair {\n        valueType = TYPE_STRING\n        this.value = value.toByteArray()\n        return this\n    }\n\n    fun put(value: Set<String>): KeyValuePair {\n        valueType = TYPE_STRING_SET\n        val stream = ByteArrayOutputStream()\n        val intBuffer = ByteBuffer.allocate(4)\n        for (v in value) {\n            intBuffer.rewind()\n            stream.write(intBuffer.putInt(v.length).array())\n            stream.write(v.toByteArray())\n        }\n        this.value = stream.toByteArray()\n        return this\n    }\n\n    @Suppress(\"IMPLICIT_CAST_TO_ANY\")\n    override fun toString(): String {\n        return when (valueType) {\n            TYPE_BOOLEAN -> boolean\n            TYPE_FLOAT -> float\n            TYPE_LONG -> long\n            TYPE_STRING -> string\n            TYPE_STRING_SET -> stringSet\n            else -> null\n        }?.toString() ?: \"null\"\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/preference/OnPreferenceDataStoreChangeListener.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database.preference\n\nimport androidx.preference.PreferenceDataStore\n\ninterface OnPreferenceDataStoreChangeListener {\n    fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database.preference\n\nimport androidx.room.Database\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport dev.matrix.roomigrant.GenerateRoomMigrations\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.SagerNet\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\n\n@Database(entities = [KeyValuePair::class], version = 1)\n@GenerateRoomMigrations\nabstract class PublicDatabase : RoomDatabase() {\n    companion object {\n        private val instance by lazy {\n            SagerNet.deviceStorage.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs()\n            Room.databaseBuilder(SagerNet.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)\n                .allowMainThreadQueries()\n                .enableMultiInstanceInvalidation()\n                .fallbackToDestructiveMigration()\n                .setQueryExecutor { GlobalScope.launch { it.run() } }\n                .build()\n        }\n\n        val kvPairDao get() = instance.keyValuePairDao()\n    }\n\n    abstract fun keyValuePairDao(): KeyValuePair.Dao\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/database/preference/RoomPreferenceDataStore.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.database.preference\n\nimport androidx.preference.PreferenceDataStore\n\n@Suppress(\"MemberVisibilityCanBePrivate\", \"unused\")\nopen class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :\n    PreferenceDataStore() {\n\n    fun getBoolean(key: String) = kvPairDao[key]?.boolean\n    fun getFloat(key: String) = kvPairDao[key]?.float\n    fun getInt(key: String) = kvPairDao[key]?.long?.toInt()\n    fun getLong(key: String) = kvPairDao[key]?.long\n    fun getString(key: String) = kvPairDao[key]?.string\n    fun getStringSet(key: String) = kvPairDao[key]?.stringSet\n\n    override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue\n    override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue\n    override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue\n    override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue\n    override fun getString(key: String, defValue: String?) = getString(key) ?: defValue\n    override fun getStringSet(key: String, defValue: MutableSet<String>?) =\n        getStringSet(key) ?: defValue\n\n    fun putBoolean(key: String, value: Boolean?) =\n        if (value == null) remove(key) else putBoolean(key, value)\n\n    fun putFloat(key: String, value: Float?) =\n        if (value == null) remove(key) else putFloat(key, value)\n\n    fun putInt(key: String, value: Int?) =\n        if (value == null) remove(key) else putLong(key, value.toLong())\n\n    fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)\n    override fun putBoolean(key: String, value: Boolean) {\n        kvPairDao.put(KeyValuePair(key).put(value))\n        fireChangeListener(key)\n    }\n\n    override fun putFloat(key: String, value: Float) {\n        kvPairDao.put(KeyValuePair(key).put(value))\n        fireChangeListener(key)\n    }\n\n    override fun putInt(key: String, value: Int) {\n        kvPairDao.put(KeyValuePair(key).put(value.toLong()))\n        fireChangeListener(key)\n    }\n\n    override fun putLong(key: String, value: Long) {\n        kvPairDao.put(KeyValuePair(key).put(value))\n        fireChangeListener(key)\n    }\n\n    override fun putString(key: String, value: String?) = if (value == null) remove(key) else {\n        kvPairDao.put(KeyValuePair(key).put(value))\n        fireChangeListener(key)\n    }\n\n    override fun putStringSet(key: String, values: MutableSet<String>?) =\n        if (values == null) remove(key) else {\n            kvPairDao.put(KeyValuePair(key).put(values))\n            fireChangeListener(key)\n        }\n\n    fun remove(key: String) {\n        kvPairDao.delete(key)\n        fireChangeListener(key)\n    }\n\n    private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()\n    private fun fireChangeListener(key: String) {\n        val listeners = synchronized(listeners) {\n            listeners.toList()\n        }\n        listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }\n    }\n\n    fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) {\n        synchronized(listeners) {\n            listeners.add(listener)\n        }\n    }\n\n    fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) {\n        synchronized(listeners) {\n            listeners.remove(listener)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport cn.hutool.core.clone.Cloneable;\nimport cn.hutool.core.util.StrUtil;\nimport cn.hutool.json.JSONUtil;\nimport io.nekohasekai.sagernet.ExtraType;\nimport io.nekohasekai.sagernet.fmt.gson.GsonsKt;\nimport io.nekohasekai.sagernet.ktx.KryosKt;\nimport io.nekohasekai.sagernet.ktx.NetsKt;\n\npublic abstract class AbstractBean extends Serializable implements Cloneable<AbstractBean> {\n\n    public String serverAddress;\n    public Integer serverPort;\n    public String name;\n\n    public transient boolean isChain;\n    public transient String finalAddress;\n    public transient int finalPort;\n\n    public int extraType;\n    public String profileId;\n    public String group;\n    public String owner;\n    public List<String> tags;\n\n    public String displayName() {\n        if (StrUtil.isNotBlank(name)) {\n            return name;\n        } else {\n            return serverAddress + \":\" + serverPort;\n        }\n    }\n\n    public String displayAddress() {\n        return serverAddress + \":\" + serverPort;\n    }\n\n    public String network() {\n        return \"tcp,udp\";\n    }\n\n    public boolean canICMPing() {\n        return true;\n    }\n\n    public boolean canTCPing() {\n        return true;\n    }\n\n    public boolean canMapping() {\n        return true;\n    }\n\n    @Override\n    public void initializeDefaultValues() {\n        if (StrUtil.isBlank(serverAddress)) {\n            serverAddress = \"127.0.0.1\";\n        } else if (serverAddress.startsWith(\"[\") && serverAddress.endsWith(\"]\")) {\n            serverAddress = NetsKt.unwrapHost(serverAddress);\n        }\n        if (serverPort == null) {\n            serverPort = 1080;\n        }\n        if (name == null) name = \"\";\n\n        finalAddress = serverAddress;\n        finalPort = serverPort;\n\n        if (profileId == null) profileId = \"\";\n        if (group == null) group = \"\";\n        if (tags == null) tags = new ArrayList<>();\n    }\n\n\n    private transient boolean serializeWithoutName;\n\n    @Override\n    public void serializeToBuffer(@NonNull ByteBufferOutput output) {\n        serialize(output);\n\n        output.writeInt(1);\n        if (!serializeWithoutName) {\n            output.writeString(name);\n        }\n        output.writeInt(extraType);\n        if (extraType == ExtraType.NONE) return;\n        output.writeString(profileId);\n        if (extraType == ExtraType.OOCv1) {\n            output.writeString(group);\n            output.writeString(owner);\n            KryosKt.writeStringList(output, tags);\n        }\n    }\n\n    @Override\n    public void deserializeFromBuffer(@NonNull ByteBufferInput input) {\n        deserialize(input);\n\n        int extraVersion = input.readInt();\n\n        name = input.readString();\n        extraType = input.readInt();\n        if (extraType == ExtraType.NONE) return;\n        profileId = input.readString();\n\n        if (extraType == ExtraType.OOCv1) {\n            group = input.readString();\n            if (extraVersion >= 1) {\n                owner = input.readString();\n            }\n            tags = KryosKt.readStringList(input);\n        }\n    }\n\n    public void serialize(ByteBufferOutput output) {\n        output.writeString(serverAddress);\n        output.writeInt(serverPort);\n    }\n\n    public void deserialize(ByteBufferInput input) {\n        serverAddress = input.readString();\n        serverPort = input.readInt();\n    }\n\n    @NotNull\n    @Override\n    public abstract AbstractBean clone();\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        try {\n            serializeWithoutName = true;\n            ((AbstractBean) o).serializeWithoutName = true;\n            return Arrays.equals(KryoConverters.serialize(this), KryoConverters.serialize((AbstractBean) o));\n        } finally {\n            serializeWithoutName = false;\n            ((AbstractBean) o).serializeWithoutName = false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        try {\n            serializeWithoutName = true;\n            return Arrays.hashCode(KryoConverters.serialize(this));\n        } finally {\n            serializeWithoutName = false;\n        }\n    }\n\n    @NotNull\n    @Override\n    public String toString() {\n        return getClass().getSimpleName() + \" \" + JSONUtil.formatJsonStr(GsonsKt.getGson().toJson(this));\n    }\n\n    public void applyFeatureSettings(AbstractBean other) {\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt\n\nimport android.os.Build\nimport cn.hutool.core.util.NumberUtil\nimport cn.hutool.json.JSONArray\nimport cn.hutool.json.JSONObject\nimport com.google.gson.JsonSyntaxException\nimport io.nekohasekai.sagernet.IPv6Mode\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.bg.ForegroundDetectorService\nimport io.nekohasekai.sagernet.bg.VpnService\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.fmt.V2rayBuildResult.IndexEntity\nimport io.nekohasekai.sagernet.fmt.brook.BrookBean\nimport io.nekohasekai.sagernet.fmt.gson.gson\nimport io.nekohasekai.sagernet.fmt.http.HttpBean\nimport io.nekohasekai.sagernet.fmt.internal.BalancerBean\nimport io.nekohasekai.sagernet.fmt.internal.ChainBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean\nimport io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean\nimport io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig\nimport io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig.*\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean\nimport io.nekohasekai.sagernet.ktx.isIpAddress\nimport io.nekohasekai.sagernet.ktx.isRunning\nimport io.nekohasekai.sagernet.ktx.mkPort\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nconst val TAG_SOCKS = \"socks\"\nconst val TAG_HTTP = \"http\"\nconst val TAG_TRANS = \"trans\"\n\nconst val TAG_AGENT = \"proxy\"\nconst val TAG_DIRECT = \"direct\"\nconst val TAG_BYPASS = \"bypass\"\nconst val TAG_BLOCK = \"block\"\n\nconst val TAG_DNS_IN = \"dns-in\"\nconst val TAG_DNS_OUT = \"dns-out\"\n\nconst val TAG_API_IN = \"api-in\"\n\nconst val LOCALHOST = \"127.0.0.1\"\nconst val IP6_LOCALHOST = \"::1\"\n\nclass V2rayBuildResult(\n    var config: String,\n    var index: List<IndexEntity>,\n    var requireWs: Boolean,\n    var outboundTags: List<String>,\n    var outboundTagsCurrent: List<String>,\n    var outboundTagsAll: Map<String, ProxyEntity>,\n    var bypassTag: String,\n    var observatoryTags: Set<String>,\n    val dumpUid: Boolean,\n    val alerts: List<Pair<Int, String>>,\n) {\n    data class IndexEntity(var isBalancer: Boolean, var chain: LinkedHashMap<Int, ProxyEntity>)\n}\n\nfun buildV2RayConfig(\n    proxy: ProxyEntity, forTest: Boolean = false\n): V2rayBuildResult {\n\n    val outboundTags = ArrayList<String>()\n    val outboundTagsCurrent = ArrayList<String>()\n    val outboundTagsAll = HashMap<String, ProxyEntity>()\n    val globalOutbounds = ArrayList<String>()\n\n    fun ProxyEntity.resolveChain(): MutableList<ProxyEntity> {\n        val bean = requireBean()\n        if (bean is ChainBean) {\n            val beans = SagerDatabase.proxyDao.getEntities(bean.proxies)\n            val beansMap = beans.associateBy { it.id }\n            val beanList = ArrayList<ProxyEntity>()\n            for (proxyId in bean.proxies) {\n                val item = beansMap[proxyId] ?: continue\n                when (item.type) {\n                    ProxyEntity.TYPE_BALANCER -> error(\"Balancer is incompatible with chain\")\n                    ProxyEntity.TYPE_CONFIG -> error(\"Custom config is incompatible with chain\")\n                }\n                beanList.addAll(item.resolveChain())\n            }\n            return beanList.asReversed()\n        } else if (bean is BalancerBean) {\n            val beans = if (bean.type == BalancerBean.TYPE_LIST) {\n                SagerDatabase.proxyDao.getEntities(bean.proxies)\n            } else {\n                SagerDatabase.proxyDao.getByGroup(bean.groupId)\n            }\n\n            val beansMap = beans.associateBy { it.id }\n            val beanList = ArrayList<ProxyEntity>()\n            for (proxyId in beansMap.keys) {\n                val item = beansMap[proxyId] ?: continue\n                if (item.id == id) continue\n                when (item.type) {\n                    ProxyEntity.TYPE_BALANCER -> error(\"Nested balancers are not supported\")\n                    ProxyEntity.TYPE_CHAIN -> error(\"Chain is incompatible with balancer\")\n                }\n                beanList.add(item)\n            }\n            return beanList\n        }\n        return mutableListOf(this)\n    }\n\n    val proxies = proxy.resolveChain()\n    val extraRules = if (forTest) listOf() else SagerDatabase.rulesDao.enabledRules()\n    val extraProxies = if (forTest) mapOf() else SagerDatabase.proxyDao.getEntities(extraRules.mapNotNull { rule ->\n        rule.outbound.takeIf { it > 0 && it != proxy.id }\n    }.toHashSet().toList()).associate {\n        (it.id to ((it.type == ProxyEntity.TYPE_BALANCER) to lazy {\n            it.balancerBean!!.strategy\n        })) to it.resolveChain()\n    }\n\n    val allowAccess = DataStore.allowAccess\n    val bind = if (!forTest && allowAccess) \"0.0.0.0\" else LOCALHOST\n\n    val remoteDns = DataStore.remoteDns.split(\"\\n\")\n        .mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith(\"#\") } }\n    val directDNS = DataStore.directDns.split(\"\\n\")\n        .mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith(\"#\") } }\n    val enableDnsRouting = DataStore.enableDnsRouting\n    val useFakeDns = DataStore.enableFakeDns\n    val trafficSniffing = DataStore.trafficSniffing\n    val indexMap = ArrayList<IndexEntity>()\n    var requireWs = false\n    val requireHttp = !forTest && (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M || DataStore.requireHttp)\n    val requireTransproxy = if (forTest) false else DataStore.requireTransproxy\n    val ipv6Mode = if (forTest) IPv6Mode.ENABLE else DataStore.ipv6Mode\n    val resolveDestination = DataStore.resolveDestination\n    val destinationOverride = DataStore.destinationOverride\n    val trafficStatistics = !forTest && DataStore.profileTrafficStatistics\n\n    val outboundDomainStrategy = when {\n        !resolveDestination -> \"AsIs\"\n        ipv6Mode == IPv6Mode.DISABLE -> \"UseIPv4\"\n        ipv6Mode == IPv6Mode.PREFER -> \"PreferIPv6\"\n        ipv6Mode == IPv6Mode.ONLY -> \"UseIPv6\"\n        else -> \"PreferIPv4\"\n    }\n\n    var dumpUid = false\n    val alerts = mutableListOf<Pair<Int, String>>()\n\n    return V2RayConfig().apply {\n\n        dns = DnsObject().apply {\n            hosts = DataStore.hosts.split(\"\\n\")\n                .filter { it.isNotBlank() }\n                .associate { it.substringBefore(\" \") to it.substringAfter(\" \") }\n                .toMutableMap()\n            servers = mutableListOf()\n\n            servers.addAll(remoteDns.map {\n                DnsObject.StringOrServerObject().apply {\n                    valueY = DnsObject.ServerObject().apply {\n                        address = it\n                        concurrent = true\n                    }\n                }\n            })\n\n            disableFallbackIfMatch = true\n\n            if (useFakeDns) {\n                fakedns = FakeDnsObject().apply {\n                    ipPool = if (ipv6Mode == IPv6Mode.ONLY) {\n                        \"${VpnService.FAKEDNS_VLAN6_CLIENT}/18\"\n                    } else {\n                        \"${VpnService.FAKEDNS_VLAN4_CLIENT}/15\"\n                    }\n                    poolSize = 65535\n                }\n            }\n\n            when (ipv6Mode) {\n                IPv6Mode.DISABLE -> {\n                    queryStrategy = \"UseIPv4\"\n                }\n                IPv6Mode.ONLY -> {\n                    queryStrategy = \"UseIPv6\"\n                }\n            }\n        }\n\n        log = LogObject().apply {\n            loglevel = if (DataStore.enableLog) \"debug\" else \"error\"\n        }\n\n        policy = PolicyObject().apply {\n            levels = mapOf(\n                // dns\n                \"1\" to PolicyObject.LevelPolicyObject().apply {\n                    connIdle = 30\n                })\n\n            if (trafficStatistics) {\n                system = PolicyObject.SystemPolicyObject().apply {\n                    statsOutboundDownlink = true\n                    statsOutboundUplink = true\n                }\n            }\n        }\n        inbounds = mutableListOf()\n\n        if (!forTest) inbounds.add(InboundObject().apply {\n            tag = TAG_SOCKS\n            listen = bind\n            port = DataStore.socksPort\n            protocol = \"socks\"\n            settings = LazyInboundConfigurationObject(this,\n                SocksInboundConfigurationObject().apply {\n                    auth = \"noauth\"\n                    udp = true\n                })\n            if (trafficSniffing || useFakeDns) {\n                sniffing = InboundObject.SniffingObject().apply {\n                    enabled = true\n                    destOverride = when {\n                        useFakeDns && !trafficSniffing -> listOf(\"fakedns\")\n                        useFakeDns -> listOf(\"fakedns\", \"http\", \"tls\")\n                        else -> listOf(\"http\", \"tls\")\n                    }\n                    metadataOnly = useFakeDns && !trafficSniffing\n                    routeOnly = !destinationOverride\n                }\n            }\n        })\n\n        if (requireHttp) {\n            inbounds.add(InboundObject().apply {\n                tag = TAG_HTTP\n                listen = bind\n                port = DataStore.httpPort\n                protocol = \"http\"\n                settings = LazyInboundConfigurationObject(this,\n                    HTTPInboundConfigurationObject().apply {\n                        allowTransparent = true\n                    })\n                if (trafficSniffing || useFakeDns) {\n                    sniffing = InboundObject.SniffingObject().apply {\n                        enabled = true\n                        destOverride = when {\n                            useFakeDns && !trafficSniffing -> listOf(\"fakedns\")\n                            useFakeDns -> listOf(\"fakedns\", \"http\", \"tls\")\n                            else -> listOf(\"http\", \"tls\")\n                        }\n                        metadataOnly = useFakeDns && !trafficSniffing\n                        routeOnly = !destinationOverride\n                    }\n                }\n            })\n        }\n\n        if (requireTransproxy) {\n            inbounds.add(InboundObject().apply {\n                tag = TAG_TRANS\n                listen = bind\n                port = DataStore.transproxyPort\n                protocol = \"dokodemo-door\"\n                settings = LazyInboundConfigurationObject(this,\n                    DokodemoDoorInboundConfigurationObject().apply {\n                        network = \"tcp,udp\"\n                        followRedirect = true\n                    })\n                if (trafficSniffing || useFakeDns) {\n                    sniffing = InboundObject.SniffingObject().apply {\n                        enabled = true\n                        destOverride = when {\n                            useFakeDns && !trafficSniffing -> listOf(\"fakedns\")\n                            useFakeDns -> listOf(\"fakedns\", \"http\", \"tls\")\n                            else -> listOf(\"http\", \"tls\")\n                        }\n                        metadataOnly = useFakeDns && !trafficSniffing\n                        routeOnly = !destinationOverride\n                    }\n                }\n                when (DataStore.transproxyMode) {\n                    1 -> streamSettings = StreamSettingsObject().apply {\n                        sockopt = StreamSettingsObject.SockoptObject().apply {\n                            tproxy = \"tproxy\"\n                        }\n                    }\n                }\n            })\n        }\n\n        outbounds = mutableListOf()\n\n        routing = RoutingObject().apply {\n            domainStrategy = DataStore.domainStrategy\n\n            rules = mutableListOf()\n\n            val wsRules = HashMap<String, RoutingObject.RuleObject>()\n\n            for (proxyEntity in proxies) {\n                val bean = proxyEntity.requireBean()\n\n                if (bean is StandardV2RayBean && bean.type == \"ws\" && bean.wsUseBrowserForwarder == true) {\n                    val route = RoutingObject.RuleObject().apply {\n                        type = \"field\"\n                        outboundTag = TAG_DIRECT\n                        when {\n                            bean.host.isIpAddress() -> {\n                                ip = listOf(bean.host)\n                            }\n                            bean.host.isNotBlank() -> {\n                                domain = listOf(bean.host)\n                            }\n                            bean.serverAddress.isIpAddress() -> {\n                                ip = listOf(bean.serverAddress)\n                            }\n                            else -> domain = listOf(bean.serverAddress)\n                        }\n                    }\n                    wsRules[bean.host.takeIf { !it.isNullOrBlank() } ?: bean.serverAddress] = route\n                }\n            }\n\n            rules.addAll(wsRules.values)\n\n            if (DataStore.bypassLan && (requireHttp || DataStore.bypassLanInCoreOnly)) {\n                rules.add(RoutingObject.RuleObject().apply {\n                    type = \"field\"\n                    outboundTag = TAG_BYPASS\n                    ip = listOf(\"geoip:private\")\n                })\n            }\n        }\n\n        val needIncludeSelf = proxy.balancerBean == null && proxies.size > 1 || extraProxies.any { (key, value) ->\n            val (_, balancer) = key\n            val (isBalancer, _) = balancer\n            isBalancer && value.size > 1\n        }\n\n        var rootBalancer: RoutingObject.RuleObject? = null\n\n        val utlsFingerprint = DataStore.utlsFingerprint\n        fun buildChain(\n            tagOutbound: String,\n            profileList: List<ProxyEntity>,\n            isBalancer: Boolean,\n            balancerStrategy: (() -> String)\n        ): String {\n            var pastExternal = false\n            lateinit var pastOutbound: OutboundObject\n            lateinit var currentOutbound: OutboundObject\n            lateinit var pastInboundTag: String\n            val chainMap = LinkedHashMap<Int, ProxyEntity>()\n            indexMap.add(IndexEntity(isBalancer, chainMap))\n            val chainOutbounds = ArrayList<OutboundObject>()\n            var chainOutbound = \"\"\n\n            profileList.forEachIndexed { index, proxyEntity ->\n                val bean = proxyEntity.requireBean()\n                currentOutbound = OutboundObject()\n\n                val tagIn: String\n                val needGlobal: Boolean\n\n                if (isBalancer || index == profileList.lastIndex && !pastExternal) {\n                    tagIn = \"$TAG_AGENT-global-${proxyEntity.id}\"\n                    needGlobal = true\n                } else {\n                    tagIn = if (index == 0) tagOutbound else {\n                        \"$tagOutbound-${proxyEntity.id}\"\n                    }\n                    needGlobal = false\n                }\n\n                if (index == profileList.lastIndex) {\n                    chainOutbound = tagIn\n                }\n\n                if (needGlobal) {\n                    if (globalOutbounds.contains(tagIn)) {\n                        return@forEachIndexed\n                    }\n                    globalOutbounds.add(tagIn)\n                }\n\n                outboundTagsAll[tagIn] = proxyEntity\n\n                if (isBalancer || index == 0) {\n                    outboundTags.add(tagIn)\n                    if (tagOutbound == TAG_AGENT) {\n                        outboundTagsCurrent.add(tagIn)\n                    }\n                }\n\n                if (proxyEntity.needExternal()) {\n                    val localPort = mkPort()\n                    chainMap[localPort] = proxyEntity\n                    currentOutbound.apply {\n                        protocol = \"socks\"\n                        settings = LazyOutboundConfigurationObject(this,\n                            SocksOutboundConfigurationObject().apply {\n                                servers = listOf(SocksOutboundConfigurationObject.ServerObject()\n                                    .apply {\n                                        address = LOCALHOST\n                                        port = localPort\n                                    })\n                            })\n                    }\n                } else {\n                    currentOutbound.apply {\n                        if (bean is SOCKSBean) {\n                            protocol = \"socks\"\n                            settings = LazyOutboundConfigurationObject(this,\n                                SocksOutboundConfigurationObject().apply {\n                                    servers = listOf(SocksOutboundConfigurationObject.ServerObject()\n                                        .apply {\n                                            address = bean.serverAddress\n                                            port = bean.serverPort\n                                            if (!bean.username.isNullOrBlank()) {\n                                                users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()\n                                                    .apply {\n                                                        user = bean.username\n                                                        pass = bean.password\n                                                    })\n                                            }\n                                        })\n                                    version = bean.protocolVersionName()\n                                })\n                            if (bean.tls) {\n                                streamSettings = StreamSettingsObject().apply {\n                                    network = \"tcp\"\n                                    if (bean.tls) {\n                                        security = \"tls\"\n                                        tlsSettings = TLSObject().apply {\n                                            if (bean.sni.isNotBlank()) {\n                                                serverName = bean.sni\n                                            }\n\n                                            if (utlsFingerprint.isNotBlank()) {\n                                                fingerprint = utlsFingerprint\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        } else if (bean is HttpBean) {\n                            protocol = \"http\"\n                            settings = LazyOutboundConfigurationObject(this,\n                                HTTPOutboundConfigurationObject().apply {\n                                    servers = listOf(HTTPOutboundConfigurationObject.ServerObject()\n                                        .apply {\n                                            address = bean.serverAddress\n                                            port = bean.serverPort\n                                            if (!bean.username.isNullOrBlank()) {\n                                                users = listOf(HTTPInboundConfigurationObject.AccountObject()\n                                                    .apply {\n                                                        user = bean.username\n                                                        pass = bean.password\n                                                    })\n                                            }\n                                        })\n                                })\n                            if (bean.tls) {\n                                streamSettings = StreamSettingsObject().apply {\n                                    network = \"tcp\"\n                                    if (bean.tls) {\n                                        security = \"tls\"\n                                        tlsSettings = TLSObject().apply {\n                                            if (bean.sni.isNotBlank()) {\n                                                serverName = bean.sni\n                                            }\n                                            if (utlsFingerprint.isNotBlank()) {\n                                                fingerprint = utlsFingerprint\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        } else if (bean is StandardV2RayBean) {\n                            if (bean is VMessBean) {\n                                protocol = \"vmess\"\n                                settings = LazyOutboundConfigurationObject(this,\n                                    VMessOutboundConfigurationObject().apply {\n                                        vnext = listOf(VMessOutboundConfigurationObject.ServerObject()\n                                            .apply {\n                                                address = bean.serverAddress\n                                                port = bean.serverPort\n                                                users = listOf(VMessOutboundConfigurationObject.ServerObject.UserObject()\n                                                    .apply {\n                                                        id = bean.uuidOrGenerate()\n                                                        alterId = bean.alterId\n                                                        security = bean.encryption.takeIf { it.isNotBlank() }\n                                                            ?: \"auto\"\n                                                        experimental = \"\"\n                                                        if (bean.experimentalAuthenticatedLength) {\n                                                            experimental += \"AuthenticatedLength\"\n                                                        }\n                                                        if (bean.experimentalNoTerminationSignal) {\n                                                            experimental += \"NoTerminationSignal\"\n                                                        }\n                                                        if (experimental.isBlank()) experimental = null;\n                                                    })\n                                            })\n                                    })\n                            } else if (bean is VLESSBean) {\n                                protocol = \"vless\"\n                                settings = LazyOutboundConfigurationObject(this,\n                                    VLESSOutboundConfigurationObject().apply {\n                                        vnext = listOf(VLESSOutboundConfigurationObject.ServerObject()\n                                            .apply {\n                                                address = bean.serverAddress\n                                                port = bean.serverPort\n                                                users = listOf(VLESSOutboundConfigurationObject.ServerObject.UserObject()\n                                                    .apply {\n                                                        id = bean.uuidOrGenerate()\n                                                        encryption = bean.encryption\n                                                        if (bean.security == \"xtls\") {\n                                                            flow = bean.flow\n                                                        }\n                                                    })\n                                            })\n                                    })\n                            }\n\n                            streamSettings = StreamSettingsObject().apply {\n                                network = bean.type\n                                if (bean.security.isNotBlank()) {\n                                    security = bean.security\n                                }\n                                val settings = TLSObject().apply {\n                                    if (bean.sni.isNotBlank()) {\n                                        serverName = bean.sni\n                                    }\n\n                                    if (bean.alpn.isNotBlank()) {\n                                        alpn = bean.alpn.split(\"\\n\")\n                                    }\n\n                                    if (bean.certificates.isNotBlank()) {\n                                        disableSystemRoot = true\n                                        certificates = listOf(TLSObject.CertificateObject().apply {\n                                            usage = \"verify\"\n                                            certificate = bean.certificates.split(\n                                                \"\\n\"\n                                            ).filter { it.isNotBlank() }\n                                        })\n                                    }\n\n                                    if (bean.allowInsecure) {\n                                        allowInsecure = true\n                                    }\n\n                                    if (utlsFingerprint.isNotBlank()) {\n                                        fingerprint = utlsFingerprint\n                                    }\n                                }\n                                when (security) {\n                                    \"tls\" -> tlsSettings = settings\n                                    \"xtls\" -> xtlsSettings = settings\n                                }\n\n                                when (network) {\n                                    \"tcp\" -> {\n                                        tcpSettings = TcpObject().apply {\n                                            if (bean.headerType == \"http\") {\n                                                header = TcpObject.HeaderObject().apply {\n                                                    type = \"http\"\n                                                    if (bean.host.isNotBlank() || bean.path.isNotBlank()) {\n                                                        request = TcpObject.HeaderObject.HTTPRequestObject()\n                                                            .apply {\n                                                                headers = mutableMapOf()\n                                                                if (bean.host.isNotBlank()) {\n                                                                    headers[\"Host\"] = TcpObject.HeaderObject.StringOrListObject()\n                                                                        .apply {\n                                                                            valueY = bean.host.split(\n                                                                                \",\"\n                                                                            ).map { it.trim() }\n                                                                        }\n                                                                }\n                                                                if (bean.path.isNotBlank()) {\n                                                                    path = bean.path.split(\",\")\n                                                                }\n                                                            }\n                                                    }\n                                                }\n                                            }\n                                        }\n                                    }\n                                    \"kcp\" -> {\n                                        kcpSettings = KcpObject().apply {\n                                            mtu = 1350\n                                            tti = 50\n                                            uplinkCapacity = 12\n                                            downlinkCapacity = 100\n                                            congestion = false\n                                            readBufferSize = 1\n                                            writeBufferSize = 1\n                                            header = KcpObject.HeaderObject().apply {\n                                                type = bean.headerType\n                                            }\n                                            if (bean.mKcpSeed.isNotBlank()) {\n                                                seed = bean.mKcpSeed\n                                            }\n                                        }\n                                    }\n                                    \"ws\" -> {\n                                        wsSettings = WebSocketObject().apply {\n                                            headers = mutableMapOf()\n\n                                            if (bean.host.isNotBlank()) {\n                                                headers[\"Host\"] = bean.host\n                                            }\n\n                                            path = bean.path.takeIf { it.isNotBlank() } ?: \"/\"\n\n                                            if (bean.wsUseBrowserForwarder) {\n                                                requireWs = true\n                                            }\n                                        }\n                                    }\n                                    \"http\" -> {\n                                        network = \"http\"\n\n                                        httpSettings = HttpObject().apply {\n                                            if (bean.host.isNotBlank()) {\n                                                host = bean.host.split(\",\")\n                                            }\n\n                                            path = bean.path.takeIf { it.isNotBlank() } ?: \"/\"\n                                        }\n                                    }\n                                    \"quic\" -> {\n                                        quicSettings = QuicObject().apply {\n                                            security = bean.quicSecurity.takeIf { it.isNotBlank() }\n                                                ?: \"none\"\n                                            key = bean.quicKey\n                                            header = QuicObject.HeaderObject().apply {\n                                                type = bean.headerType.takeIf { it.isNotBlank() }\n                                                    ?: \"none\"\n                                            }\n                                        }\n                                    }\n                                    \"grpc\" -> {\n                                        grpcSettings = GrpcObject().apply {\n                                            serviceName = bean.grpcServiceName\n                                            multiMode = bean.grpcMultiMode\n                                        }\n                                    }\n                                }\n                            }\n                        } else if (bean is ShadowsocksBean) {\n                            protocol = \"shadowsocks\"\n                            settings = LazyOutboundConfigurationObject(this,\n                                ShadowsocksOutboundConfigurationObject().apply {\n                                    servers = listOf(ShadowsocksOutboundConfigurationObject.ServerObject()\n                                        .apply {\n                                            address = bean.serverAddress\n                                            port = bean.serverPort\n                                            method = bean.method\n                                            password = bean.password\n                                        })\n                                })\n                        } else if (bean is TrojanBean) {\n                            protocol = \"trojan\"\n                            settings = LazyOutboundConfigurationObject(this,\n                                TrojanOutboundConfigurationObject().apply {\n                                    servers = listOf(TrojanOutboundConfigurationObject.ServerObject()\n                                        .apply {\n                                            address = bean.serverAddress\n                                            port = bean.serverPort\n                                            password = bean.password\n                                            if (bean.security == \"xtls\") {\n                                                flow = bean.flow\n                                            }\n                                        })\n                                })\n                            streamSettings = StreamSettingsObject().apply {\n                                network = \"tcp\"\n                                security = bean.security\n                                val settings = TLSObject().apply {\n                                    if (bean.sni.isNotBlank()) {\n                                        serverName = bean.sni\n                                    }\n                                    if (bean.alpn.isNotBlank()) {\n                                        alpn = bean.alpn.split(\"\\n\")\n                                    }\n                                    if (utlsFingerprint.isNotBlank()) {\n                                        fingerprint = utlsFingerprint\n                                    }\n                                }\n                                when (security) {\n                                    \"tls\" -> tlsSettings = settings\n                                    \"xtls\" -> xtlsSettings = settings\n                                }\n                            }\n                        }\n                        if ((isBalancer || index == 0) && proxyEntity.needCoreMux() && DataStore.enableMux) {\n                            mux = OutboundObject.MuxObject().apply {\n                                enabled = true\n                                concurrency = DataStore.muxConcurrency\n                            }\n                        }\n                    }\n                }\n\n                currentOutbound.tag = tagIn\n                currentOutbound.domainStrategy = outboundDomainStrategy\n\n                if (!isBalancer && index > 0) {\n                    if (!pastExternal) {\n                        pastOutbound.proxySettings = OutboundObject.ProxySettingsObject().apply {\n                            tag = tagIn\n                            transportLayer = true\n                        }\n                    } else {\n                        routing.rules.add(RoutingObject.RuleObject().apply {\n                            type = \"field\"\n                            inboundTag = listOf(pastInboundTag)\n                            outboundTag = tagIn\n                        })\n                    }\n                }\n\n                if (proxyEntity.needExternal() && !isBalancer && index != profileList.lastIndex) {\n                    val mappingPort = mkPort()\n                    when (bean) {\n                        is BrookBean -> {\n                            dns.hosts[bean.serverAddress] = LOCALHOST\n                        }\n                        else -> {\n                            bean.finalAddress = LOCALHOST\n                        }\n                    }\n                    bean.finalPort = mappingPort\n                    bean.isChain = true\n\n                    inbounds.add(InboundObject().apply {\n                        listen = LOCALHOST\n                        port = mappingPort\n                        tag = \"$tagOutbound-mapping-${proxyEntity.id}\"\n                        protocol = \"dokodemo-door\"\n                        settings = LazyInboundConfigurationObject(this,\n                            DokodemoDoorInboundConfigurationObject().apply {\n                                address = bean.serverAddress\n                                network = bean.network()\n                                port = bean.serverPort\n                            })\n\n                        pastInboundTag = tag\n                    })\n                } else if (bean.canMapping() && proxyEntity.needExternal() && needIncludeSelf) {\n                    val mappingPort = mkPort()\n                    when (bean) {\n                        is BrookBean -> {\n                            dns.hosts[bean.serverAddress] = LOCALHOST\n                        }\n                        else -> {\n                            bean.finalAddress = LOCALHOST\n                        }\n                    }\n                    bean.finalPort = mappingPort\n\n                    inbounds.add(InboundObject().apply {\n                        listen = LOCALHOST\n                        port = mappingPort\n                        tag = \"$tagOutbound-mapping-${proxyEntity.id}\"\n                        protocol = \"dokodemo-door\"\n                        settings = LazyInboundConfigurationObject(this,\n                            DokodemoDoorInboundConfigurationObject().apply {\n                                address = bean.serverAddress\n                                network = bean.network()\n                                port = bean.serverPort\n                            })\n                        routing.rules.add(RoutingObject.RuleObject().apply {\n                            type = \"field\"\n                            inboundTag = listOf(tag)\n                            outboundTag = TAG_DIRECT\n                        })\n                    })\n\n                }\n\n                outbounds.add(currentOutbound)\n                chainOutbounds.add(currentOutbound)\n                pastExternal = proxyEntity.needExternal()\n                pastOutbound = currentOutbound\n\n            }\n\n            if (isBalancer) {\n                if (routing.balancers == null) routing.balancers = ArrayList()\n                routing.balancers.add(RoutingObject.BalancerObject().apply {\n                    tag = \"balancer-$tagOutbound\"\n                    selector = chainOutbounds.map { it.tag }\n                    if (observatory == null) observatory = ObservatoryObject().apply {\n                        probeUrl = DataStore.connectionTestURL\n                        /*val testInterval = DataStore.probeInterval\n                        if (testInterval > 0) {\n                            probeInterval = \"${testInterval}s\"\n                        }*/\n                    }\n                    if (observatory.subjectSelector == null) observatory.subjectSelector = HashSet()\n                    observatory.subjectSelector.addAll(chainOutbounds.map { it.tag })\n                    strategy = RoutingObject.BalancerObject.StrategyObject().apply {\n                        type = balancerStrategy().takeIf { it.isNotBlank() } ?: \"random\"\n                    }\n                })\n                if (tagOutbound == TAG_AGENT) {\n                    rootBalancer = RoutingObject.RuleObject().apply {\n                        type = \"field\"\n                        inboundTag = mutableListOf()\n\n                        if (!forTest) {\n                            inboundTag.add(TAG_SOCKS)\n                        }\n                        if (requireHttp) inboundTag.add(TAG_HTTP)\n                        if (requireTransproxy) inboundTag.add(TAG_TRANS)\n                        balancerTag = \"balancer-$tagOutbound\"\n                    }\n                    /* outbounds.add(0, OutboundObject().apply {\n                         protocol = \"loopback\"\n                         settings = LazyOutboundConfigurationObject(this,\n                             LoopbackOutboundConfigurationObject().apply {\n                                 inboundTag = TAG_SOCKS\n                             })\n                     })*/\n                }\n            }\n\n            return chainOutbound\n\n        }\n\n        val tagProxy = buildChain(\n            TAG_AGENT, proxies, proxy.balancerBean != null\n        ) { proxy.balancerBean!!.strategy }\n        val balancerMap = mutableMapOf<Long, String>()\n        val tagMap = mutableMapOf<Long, String>()\n        extraProxies.forEach { (key, entities) ->\n            val (id, balancer) = key\n            val (isBalancer, strategy) = balancer\n            tagMap[id] = buildChain(\"$TAG_AGENT-$id\", entities, isBalancer, strategy::value)\n            if (isBalancer) {\n                balancerMap[id] = \"balancer-$TAG_AGENT-$id\"\n            }\n        }\n\n        val notVpn = DataStore.serviceMode != Key.MODE_VPN\n        val foregroundDetectorServiceStarted = ForegroundDetectorService::class.isRunning()\n\n        for (rule in extraRules) {\n            if (rule.packages.isNotEmpty() || rule.appStatus.isNotEmpty()) {\n                dumpUid = true\n                if (notVpn) {\n                    alerts.add(0 to rule.displayName())\n                    continue\n                }\n            }\n            if (rule.appStatus.isNotEmpty() && !foregroundDetectorServiceStarted) {\n                alerts.add(1 to rule.displayName())\n            }\n            routing.rules.add(RoutingObject.RuleObject().apply {\n                type = \"field\"\n                if (rule.packages.isNotEmpty()) {\n                    PackageCache.awaitLoadSync()\n                    uidList = rule.packages.map {\n                        PackageCache[it]?.takeIf { uid -> uid >= 10000 } ?: 1000\n                    }.toHashSet().toList()\n                }\n                if (rule.appStatus.isNotEmpty()) {\n                    appStatus = rule.appStatus\n                }\n\n                if (rule.domains.isNotBlank()) {\n                    domain = rule.domains.split(\"\\n\")\n                }\n                if (rule.ip.isNotBlank()) {\n                    ip = rule.ip.split(\"\\n\")\n                }\n                if (rule.port.isNotBlank()) {\n                    port = rule.port\n                }\n                if (rule.sourcePort.isNotBlank()) {\n                    sourcePort = rule.sourcePort\n                }\n                if (rule.network.isNotBlank()) {\n                    network = rule.network\n                }\n                if (rule.source.isNotBlank()) {\n                    source = rule.source.split(\"\\n\")\n                }\n                if (rule.protocol.isNotBlank()) {\n                    protocol = rule.protocol.split(\"\\n\")\n                }\n                if (rule.attrs.isNotBlank()) {\n                    attrs = rule.attrs\n                }\n                when {\n                    rule.reverse -> inboundTag = listOf(\"reverse-${rule.id}\")\n                    balancerMap.containsKey(rule.outbound) -> {\n                        balancerTag = balancerMap[rule.outbound]\n                    }\n                    else -> outboundTag = when (val outId = rule.outbound) {\n                        0L -> tagProxy\n                        -1L -> TAG_BYPASS\n                        -2L -> TAG_BLOCK\n                        else -> if (outId == proxy.id) tagProxy else tagMap[outId]\n                    }\n                }\n            })\n\n            if (rule.reverse) {\n                outbounds.add(OutboundObject().apply {\n                    tag = \"reverse-out-${rule.id}\"\n                    protocol = \"freedom\"\n                    settings = LazyOutboundConfigurationObject(this,\n                        FreedomOutboundConfigurationObject().apply {\n                            redirect = rule.redirect\n                        })\n                })\n                if (reverse == null) {\n                    reverse = ReverseObject().apply {\n                        bridges = ArrayList()\n                    }\n                }\n                reverse.bridges.add(ReverseObject.BridgeObject().apply {\n                    tag = \"reverse-${rule.id}\"\n                    domain = rule.domains.substringAfter(\"full:\")\n                })\n                routing.rules.add(RoutingObject.RuleObject().apply {\n                    type = \"field\"\n                    inboundTag = listOf(\"reverse-${rule.id}\")\n                    outboundTag = \"reverse-out-${rule.id}\"\n                })\n            }\n\n        }\n\n        for (freedom in arrayOf(TAG_DIRECT, TAG_BYPASS)) outbounds.add(OutboundObject().apply {\n            tag = freedom\n            protocol = \"freedom\"\n        })\n\n        outbounds.add(OutboundObject().apply {\n            tag = TAG_BLOCK\n            protocol = \"blackhole\"\n            /* settings = LazyOutboundConfigurationObject(this,\n                 BlackholeOutboundConfigurationObject().apply {\n                     keepConnection = true\n                 })*/\n        })\n\n        if (!forTest) {\n            inbounds.add(InboundObject().apply {\n                tag = TAG_DNS_IN\n                listen = bind\n                port = DataStore.localDNSPort\n                protocol = \"dokodemo-door\"\n                settings = LazyInboundConfigurationObject(this,\n                    DokodemoDoorInboundConfigurationObject().apply {\n                        address = if (!remoteDns.first().isIpAddress()) {\n                            \"1.0.0.1\"\n                        } else {\n                            remoteDns.first()\n                        }\n                        network = \"tcp,udp\"\n                        port = 53\n                    })\n\n            })\n\n            outbounds.add(OutboundObject().apply {\n                protocol = \"dns\"\n                tag = TAG_DNS_OUT\n                settings = LazyOutboundConfigurationObject(this,\n                    DNSOutboundConfigurationObject().apply {\n                        userLevel = 1\n                        var dns = remoteDns.first()\n                        if (dns.contains(\":\")) {\n                            val lPort = dns.substringAfterLast(\":\")\n                            dns = dns.substringBeforeLast(\":\")\n                            if (NumberUtil.isInteger(lPort)) {\n                                port = lPort.toInt()\n                            }\n                        }\n                        if (dns.isIpAddress()) {\n                            address = dns\n                        } else if (dns.contains(\"://\")) {\n                            network = \"tcp\"\n                            address = dns.substringAfter(\"://\")\n                        }\n                    })\n            })\n        }\n\n        for (dns in remoteDns) {\n            if (!dns.isIpAddress()) continue\n            routing.rules.add(0, RoutingObject.RuleObject().apply {\n                type = \"field\"\n                outboundTag = tagProxy\n                ip = listOf(dns)\n            })\n        }\n\n        for (dns in directDNS) {\n            if (!dns.isIpAddress()) continue\n\n            routing.rules.add(0, RoutingObject.RuleObject().apply {\n                type = \"field\"\n                outboundTag = TAG_DIRECT\n                ip = listOf(dns)\n            })\n        }\n\n        val bypassIP = HashSet<String>()\n        val bypassDomain = HashSet<String>()\n\n        (proxies + extraProxies.values.flatten()).filter { !it.requireBean().isChain }.forEach {\n            it.requireBean().apply {\n                if (!serverAddress.isIpAddress()) {\n                    bypassDomain.add(\"full:$serverAddress\")\n                } else {\n                    bypassIP.add(serverAddress)\n                }\n            }\n        }\n\n        if (bypassIP.isNotEmpty()) {\n            routing.rules.add(0, RoutingObject.RuleObject().apply {\n                type = \"field\"\n                ip = bypassIP.toList()\n                outboundTag = TAG_DIRECT\n            })\n        }\n\n        if (enableDnsRouting) {\n            for (bypassRule in extraRules.filter { it.isBypassRule() }) {\n                if (bypassRule.domains.isNotBlank()) {\n                    bypassDomain.addAll(bypassRule.domains.split(\"\\n\"))\n                }\n            }\n        }\n\n        remoteDns.forEach {\n            var address = it\n            if (address.contains(\"://\")) {\n                address = address.substringAfter(\"://\")\n            }\n            \"https://$address\".toHttpUrlOrNull()?.apply {\n                if (!host.isIpAddress()) {\n                    bypassDomain.add(\"full:$host\")\n                }\n            }\n        }\n\n        if (bypassDomain.isNotEmpty()) {\n            dns.servers.addAll(directDNS.map {\n                DnsObject.StringOrServerObject().apply {\n                    valueY = DnsObject.ServerObject().apply {\n                        address = it\n                        domains = bypassDomain.toList()\n                        skipFallback = true\n                        concurrent = true\n                    }\n                }\n            })\n        }\n\n        if (useFakeDns) {\n            dns.servers.add(0, DnsObject.StringOrServerObject().apply {\n                valueX = \"fakedns\"\n            })\n        }\n\n        if (!forTest) routing.rules.add(0, RoutingObject.RuleObject().apply {\n            type = \"field\"\n            inboundTag = listOf(TAG_DNS_IN)\n            outboundTag = TAG_DNS_OUT\n        })\n\n        if (allowAccess) {\n            // temp: fix crash\n            routing.rules.add(RoutingObject.RuleObject().apply {\n                type = \"field\"\n                ip = listOf(\"255.255.255.255\")\n                outboundTag = TAG_BLOCK\n            })\n        }\n\n        if (rootBalancer != null) routing.rules.add(rootBalancer)\n\n        if (trafficStatistics) stats = emptyMap()\n    }.let {\n        V2rayBuildResult(\n            gson.toJson(it),\n            indexMap,\n            requireWs,\n            outboundTags,\n            outboundTagsCurrent,\n            outboundTagsAll,\n            TAG_BYPASS,\n            it.observatory?.subjectSelector ?: HashSet(),\n            dumpUid,\n            alerts\n        )\n    }\n\n}\n\nfun buildCustomConfig(proxy: ProxyEntity, port: Int): V2rayBuildResult {\n\n    val bind = LOCALHOST\n    val trafficSniffing = DataStore.trafficSniffing\n\n    val bean = proxy.configBean!!\n    val config = JSONObject(bean.content)\n    val inbounds = config.getJSONArray(\"inbounds\")\n        ?.filterIsInstance<JSONObject>()\n        ?.map { gson.fromJson(it.toString(), InboundObject::class.java) }\n        ?.toMutableList() ?: ArrayList()\n\n    val dnsArr = config.getJSONObject(\"dns\")?.getJSONArray(\"servers\")?.map {\n        if (it is String) DnsObject.StringOrServerObject().apply {\n            valueX = it\n        } else DnsObject.StringOrServerObject().apply {\n            valueY = gson.fromJson(it.toString(), DnsObject.ServerObject::class.java)\n        }\n    }\n    val ipv6Mode = DataStore.ipv6Mode\n\n    var socksInbound = inbounds.find { it.tag == TAG_SOCKS }?.apply {\n        if (protocol != \"socks\") error(\"Inbound $tag with type $protocol, excepted socks.\")\n    }\n\n    if (socksInbound == null) {\n        val socksInbounds = inbounds.filter { it.protocol == \"socks\" }\n        if (socksInbounds.size == 1) {\n            socksInbound = socksInbounds[0]\n        }\n    }\n\n    if (socksInbound != null) {\n        socksInbound.apply {\n            listen = bind\n            this.port = port\n        }\n    } else {\n        inbounds.add(InboundObject().apply {\n            tag = TAG_SOCKS\n            listen = bind\n            this.port = port\n            protocol = \"socks\"\n            settings = LazyInboundConfigurationObject(this,\n                SocksInboundConfigurationObject().apply {\n                    auth = \"noauth\"\n                    udp = true\n                })\n            if (trafficSniffing) {\n                sniffing = InboundObject.SniffingObject().apply {\n                    enabled = true\n                    destOverride = listOf(\"http\", \"tls\")\n                    metadataOnly = false\n                }\n            }\n        })\n    }\n\n    var requireWs = false\n    var wsPort = 0\n    if (config.contains(\"browserForwarder\")) {\n        requireWs = true\n    }\n\n    val outbounds = try {\n        config.getJSONArray(\"outbounds\")?.filterIsInstance<JSONObject>()?.map {\n            gson.fromJson(it.toString().takeIf { it.isNotBlank() } ?: \"{}\",\n                OutboundObject::class.java)\n        }?.toMutableList()\n    } catch (e: JsonSyntaxException) {\n        null\n    }\n    var flushOutbounds = false\n\n    val outboundTags = ArrayList<String>()\n    val firstOutbound = outbounds?.get(0)\n    if (firstOutbound != null) {\n        if (firstOutbound.tag == null) {\n            firstOutbound.tag = TAG_AGENT\n            outboundTags.add(TAG_AGENT)\n            flushOutbounds = true\n        } else {\n            outboundTags.add(firstOutbound.tag)\n        }\n    }\n\n    var directTag = \"\"\n    val directOutbounds = outbounds?.filter { it.protocol == \"freedom\" }\n    if (!directOutbounds.isNullOrEmpty()) {\n        val directOutbound = if (directOutbounds.size == 1) {\n            directOutbounds[0]\n        } else {\n            val directOutboundsWithTag = directOutbounds.filter { it.tag != null }\n            if (directOutboundsWithTag.isNotEmpty()) {\n                directOutboundsWithTag[0]\n            } else {\n                directOutbounds[0]\n            }\n        }\n        if (directOutbound.tag.isNullOrBlank()) {\n            directOutbound.tag = TAG_DIRECT\n            flushOutbounds = true\n        }\n        directTag = directOutbound.tag\n    }\n\n    inbounds.forEach { it.init() }\n    config[\"inbounds\"] = JSONArray(inbounds.map { JSONObject(gson.toJson(it)) })\n    if (flushOutbounds) {\n        outbounds!!.forEach { it.init() }\n        config[\"outbounds\"] = JSONArray(outbounds.map { JSONObject(gson.toJson(it)) })\n    }\n\n    return V2rayBuildResult(\n        config.toStringPretty(),\n        emptyList(),\n        requireWs,\n        outboundTags,\n        outboundTags,\n        emptyMap(),\n        directTag,\n        emptySet(),\n        false,\n        emptyList()\n    )\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt;\n\nimport androidx.room.TypeConverter;\n\nimport com.esotericsoftware.kryo.KryoException;\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\n\nimport cn.hutool.core.io.IoUtil;\nimport cn.hutool.core.util.ArrayUtil;\nimport io.nekohasekai.sagernet.database.SubscriptionBean;\nimport io.nekohasekai.sagernet.fmt.brook.BrookBean;\nimport io.nekohasekai.sagernet.fmt.http.HttpBean;\nimport io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean;\nimport io.nekohasekai.sagernet.fmt.internal.BalancerBean;\nimport io.nekohasekai.sagernet.fmt.internal.ChainBean;\nimport io.nekohasekai.sagernet.fmt.internal.ConfigBean;\nimport io.nekohasekai.sagernet.fmt.naive.NaiveBean;\nimport io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean;\nimport io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean;\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean;\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean;\nimport io.nekohasekai.sagernet.fmt.snell.SnellBean;\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean;\nimport io.nekohasekai.sagernet.fmt.ssh.SSHBean;\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean;\nimport io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean;\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean;\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean;\nimport io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean;\nimport io.nekohasekai.sagernet.ktx.KryosKt;\nimport io.nekohasekai.sagernet.ktx.Logs;\n\npublic class KryoConverters {\n\n    private static final byte[] NULL = new byte[0];\n\n    @TypeConverter\n    public static byte[] serialize(Serializable bean) {\n        if (bean == null) return NULL;\n        ByteArrayOutputStream out = new ByteArrayOutputStream();\n        ByteBufferOutput buffer = KryosKt.byteBuffer(out);\n        bean.serializeToBuffer(buffer);\n        IoUtil.flush(buffer);\n        IoUtil.close(buffer);\n        return out.toByteArray();\n    }\n\n    public static <T extends Serializable> T deserialize(T bean, byte[] bytes) {\n        ByteArrayInputStream input = new ByteArrayInputStream(bytes);\n        ByteBufferInput buffer = KryosKt.byteBuffer(input);\n        try {\n            bean.deserializeFromBuffer(buffer);\n        } catch (KryoException e) {\n            Logs.INSTANCE.w(e);\n        }\n        bean.initializeDefaultValues();\n        return bean;\n    }\n\n    @TypeConverter\n    public static SOCKSBean socksDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new SOCKSBean(), bytes);\n    }\n\n    @TypeConverter\n    public static HttpBean httpDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new HttpBean(), bytes);\n    }\n\n    @TypeConverter\n    public static ShadowsocksBean shadowsocksDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new ShadowsocksBean(), bytes);\n    }\n\n    @TypeConverter\n    public static ShadowsocksRBean shadowsocksRDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new ShadowsocksRBean(), bytes);\n    }\n\n    @TypeConverter\n    public static VMessBean vmessDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new VMessBean(), bytes);\n    }\n\n    @TypeConverter\n    public static VLESSBean vlessDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new VLESSBean(), bytes);\n    }\n\n    @TypeConverter\n    public static TrojanBean trojanDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new TrojanBean(), bytes);\n    }\n\n    @TypeConverter\n    public static TrojanGoBean trojanGoDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new TrojanGoBean(), bytes);\n    }\n\n    @TypeConverter\n    public static NaiveBean naiveDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new NaiveBean(), bytes);\n    }\n\n    @TypeConverter\n    public static PingTunnelBean pingTunnelDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new PingTunnelBean(), bytes);\n    }\n\n    @TypeConverter\n    public static RelayBatonBean relayBatonDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new RelayBatonBean(), bytes);\n    }\n\n    @TypeConverter\n    public static BrookBean brookDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new BrookBean(), bytes);\n    }\n\n    @TypeConverter\n    public static HysteriaBean hysteriaDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new HysteriaBean(), bytes);\n    }\n\n    @TypeConverter\n    public static SnellBean snellDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new SnellBean(), bytes);\n    }\n\n    @TypeConverter\n    public static SSHBean sshDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new SSHBean(), bytes);\n    }\n\n    @TypeConverter\n    public static WireGuardBean wireguardDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new WireGuardBean(), bytes);\n    }\n\n    @TypeConverter\n    public static ConfigBean configDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new ConfigBean(), bytes);\n    }\n\n    @TypeConverter\n    public static ChainBean chainDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new ChainBean(), bytes);\n    }\n\n    @TypeConverter\n    public static BalancerBean balancerBeanDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new BalancerBean(), bytes);\n    }\n\n    @TypeConverter\n    public static SubscriptionBean subscriptionDeserialize(byte[] bytes) {\n        if (ArrayUtil.isEmpty(bytes)) return null;\n        return deserialize(new SubscriptionBean(), bytes);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt\n\nimport androidx.annotation.StringRes\nimport io.nekohasekai.sagernet.R\n\nenum class PluginEntry(\n    val pluginId: String,\n    @StringRes val nameId: Int,\n    val packageName: String,\n    val downloadSource: DownloadSource = DownloadSource()\n) {\n    // sagernet plugins\n    TrojanGo(\"trojan-go-plugin\", R.string.action_trojan_go, \"io.nekohasekai.sagernet.plugin.trojan_go\"),\n    NaiveProxy(\"naive-plugin\", R.string.action_naive, \"io.nekohasekai.sagernet.plugin.naive\"),\n    PingTunnel(\"pingtunnel-plugin\", R.string.action_ping_tunnel, \"io.nekohasekai.sagernet.plugin.pingtunnel\"),\n    RelayBaton(\"relaybaton-plugin\", R.string.action_relay_baton, \"io.nekohasekai.sagernet.plugin.relaybaton\"),\n    Brook(\"brook-plugin\", R.string.action_brook, \"io.nekohasekai.sagernet.plugin.brook\"),\n    Hysteria(\"hysteria-plugin\", R.string.action_hysteria, \"io.nekohasekai.sagernet.plugin.hysteria\", DownloadSource(fdroid = false)),\n    WireGuard(\"wireguard-plugin\", R.string.action_wireguard, \"io.nekohasekai.sagernet.plugin.wireguard\", DownloadSource(fdroid = false)),\n\n    // shadowsocks plugins\n\n    ObfsLocal(\"shadowsocks-obfs-local\", R.string.shadowsocks_plugin_simple_obfs, \"com.github.shadowsocks.plugin.obfs_local\", DownloadSource(\n        fdroid = false,\n        downloadLink = \"https://github.com/shadowsocks/simple-obfs-android/releases\"\n    )),\n\n    V2RayPlugin(\"shadowsocks-v2ray-plugin\", R.string.shadowsocks_plugin_v2ray, \"com.github.shadowsocks.plugin.v2ray\", DownloadSource(\n        downloadLink = \"https://github.com/shadowsocks/v2ray-plugin-android/releases\"\n    ));\n\n    data class DownloadSource(\n        val playStore: Boolean = true,\n        val fdroid: Boolean = true,\n        val downloadLink: String = \"https://sagernet.org/download/\"\n    )\n\n    companion object {\n\n        fun find(name: String): PluginEntry? {\n            for (pluginEntry in enumValues<PluginEntry>()) {\n                if (name == pluginEntry.pluginId) {\n                    return pluginEntry\n                }\n            }\n            return null\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/Serializable.kt",
    "content": "/******************************************************************************\n * *\n * Copyright (C) 2021 by nekohasekai <sekai></sekai>@neko.services>                    *\n * Copyright (C) 2021 by Max Lv <max.c.lv></max.c.lv>@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android></contact-shadowsocks-android>@mygod.be>  *\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 <http:></http:>//www.gnu.org/licenses/>.       *\n * *\n */\npackage io.nekohasekai.sagernet.fmt\n\nimport android.os.Parcel\nimport android.os.Parcelable\nimport com.esotericsoftware.kryo.io.ByteBufferInput\nimport com.esotericsoftware.kryo.io.ByteBufferOutput\n\nabstract class Serializable : Parcelable {\n    abstract fun initializeDefaultValues()\n    abstract fun serializeToBuffer(output: ByteBufferOutput)\n    abstract fun deserializeFromBuffer(input: ByteBufferInput)\n\n    override fun describeContents() = 0\n\n    override fun writeToParcel(dest: Parcel, flags: Int) {\n        val byteArray = KryoConverters.serialize(this)\n        dest.writeInt(byteArray.size)\n        dest.writeByteArray(byteArray)\n    }\n\n    abstract class CREATOR<T : Serializable> : Parcelable.Creator<T> {\n        abstract fun newInstance(): T\n\n        override fun createFromParcel(source: Parcel): T {\n            val byteArray = ByteArray(source.readInt())\n            source.readByteArray(byteArray)\n            return KryoConverters.deserialize(newInstance(), byteArray)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt\n\nimport io.nekohasekai.sagernet.database.ProxyEntity\n\nobject TypeMap : HashMap<String, Int>() {\n    init {\n        this[\"socks\"] = ProxyEntity.TYPE_SOCKS\n        this[\"http\"] = ProxyEntity.TYPE_HTTP\n        this[\"ss\"] = ProxyEntity.TYPE_SS\n        this[\"ssr\"] = ProxyEntity.TYPE_SSR\n        this[\"vmess\"] = ProxyEntity.TYPE_VMESS\n        this[\"vless\"] = ProxyEntity.TYPE_VLESS\n        this[\"trojan\"] = ProxyEntity.TYPE_TROJAN\n        this[\"trojan-go\"] = ProxyEntity.TYPE_TROJAN_GO\n        this[\"naive\"] = ProxyEntity.TYPE_NAIVE\n        this[\"pt\"] = ProxyEntity.TYPE_PING_TUNNEL\n        this[\"rb\"] = ProxyEntity.TYPE_RELAY_BATON\n        this[\"brook\"] = ProxyEntity.TYPE_BROOK\n        this[\"config\"] = ProxyEntity.TYPE_CONFIG\n        this[\"hysteria\"] = ProxyEntity.TYPE_HYSTERIA\n        this[\"snell\"] = ProxyEntity.TYPE_SNELL\n        this[\"ssh\"] = ProxyEntity.TYPE_SSH\n        this[\"wg\"] = ProxyEntity.TYPE_WG\n    }\n\n    val reversed = HashMap<Int, String>()\n\n    init {\n        TypeMap.forEach { (key, type) ->\n            reversed[type] = key\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/UniversalFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt\n\nimport cn.hutool.core.codec.Base64Decoder\nimport cn.hutool.core.codec.Base64Encoder\nimport cn.hutool.core.util.ZipUtil\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.database.ProxyGroup\n\nfun parseUniversal(link: String): AbstractBean {\n    return if (link.contains(\"?\")) {\n        val type = link.substringAfter(\"ax://\").substringBefore(\"?\")\n        ProxyEntity(type = TypeMap[type] ?: error(\"Type $type not found\")).apply {\n            putByteArray(ZipUtil.unZlib(Base64Decoder.decode(link.substringAfter(\"?\"))))\n        }.requireBean()\n    } else {\n        val type = link.substringAfter(\"ax://\").substringBefore(\":\")\n        ProxyEntity(type = TypeMap[type] ?: error(\"Type $type not found\")).apply {\n            putByteArray(Base64Decoder.decode(link.substringAfter(\":\").substringAfter(\":\")))\n        }.requireBean()\n    }\n}\n\nfun AbstractBean.toUniversalLink(): String {\n    var link = \"ax://\"\n    link += TypeMap.reversed[ProxyEntity().putBean(this).type]\n    link += \"?\"\n    link += Base64Encoder.encodeUrlSafe(ZipUtil.zlib(KryoConverters.serialize(this), 9))\n    return link\n}\n\n\nfun ProxyGroup.toUniversalLink(): String {\n    var link = \"sn://subscription?\"\n    export = true\n    link += Base64Encoder.encodeUrlSafe(ZipUtil.zlib(KryoConverters.serialize(this), 9))\n    export = false\n    return link\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/brook/BrookBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.brook;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class BrookBean extends AbstractBean {\n\n    public String protocol;\n    public String password;\n    public String wsPath;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (protocol == null) protocol = \"\";\n        if (password == null) password = \"\";\n        if (wsPath == null) wsPath = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(1);\n        super.serialize(output);\n        output.writeString(protocol);\n        output.writeString(password);\n        switch (protocol) {\n            case \"ws\":\n            case \"wss\": {\n                output.writeString(wsPath);\n                break;\n            }\n        }\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        protocol = input.readString();\n        password = input.readString();\n        if (version > 0) switch (protocol) {\n            case \"ws\":\n            case \"wss\": {\n                wsPath = input.readString();\n                break;\n            }\n        }\n    }\n\n    @NonNull\n    @Override\n    public BrookBean clone() {\n        return KryoConverters.deserialize(new BrookBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<BrookBean> CREATOR = new CREATOR<BrookBean>() {\n        @NonNull\n        @Override\n        public BrookBean newInstance() {\n            return new BrookBean();\n        }\n\n        @Override\n        public BrookBean[] newArray(int size) {\n            return new BrookBean[size];\n        }\n    };\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/brook/BrookFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.brook\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\nimport io.nekohasekai.sagernet.ktx.*\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nval kinds = arrayOf(\"server\", \"wsserver\", \"wssserver\", \"socks5\")\n\nfun parseBrook(text: String): AbstractBean {\n    if (!(text.contains(\"([?@])\".toRegex()))) {\n\n        // https://txthinking.github.io/brook/#/brook-link\n        // old brook scheme\n        var server = text.substringAfter(\"brook://\").unUrlSafe()\n        if (server.startsWith(\"socks5://\")) {\n            server = server.substringAfter(\"://\")\n            val bean = SOCKSBean()\n            bean.serverAddress = server.substringBefore(\":\")\n            bean.serverPort = server.substringAfter(\":\").substringBefore(\" \").toInt()\n            server = server.substringAfter(\":\").substringAfter(\" \")\n            if (server.contains(\" \")) {\n                bean.username = server.substringBefore(\" \")\n                bean.password = server.substringAfter(\" \")\n            }\n            return bean.applyDefaultValues()\n        }\n\n        val bean = BrookBean()\n\n        when {\n            server.startsWith(\"ws://\") -> {\n                bean.protocol = \"ws\"\n                server = server.substringAfter(\"://\")\n            }\n            server.startsWith(\"wss://\") -> {\n                bean.protocol = \"wss\"\n                server = server.substringAfter(\"://\")\n            }\n            else -> {\n                bean.protocol = \"\"\n            }\n        }\n\n        if (server.contains(\" \")) {\n            bean.password = server.substringAfter(\" \")\n            server = server.substringBefore(\" \")\n        }\n\n        val url =\n            \"https://$server\".toHttpUrlOrNull() ?: error(\"Invalid brook link: $text ($server)\")\n\n        bean.serverAddress = url.host\n        bean.serverPort = url.port\n        //  bean.name = url.fragment\n        if (server.contains(\"/\")) {\n            bean.wsPath = url.encodedPath.unUrlSafe()\n        }\n\n        return bean.applyDefaultValues()\n    } else if (text.matches(\"^brook://(${kinds.joinToString(\"|\")})\\\\?.+\".toRegex())) {\n\n        // https://github.com/txthinking/brook/issues/811\n\n        val link = (\"https://\" + text.substringAfter(\"://\")).toHttpUrlOrNull()\n            ?: error(\"Invalid brook url: $text\")\n\n        val bean = if (link.host == \"socks5\") SOCKSBean() else BrookBean()\n        bean.name = link.queryParameter(\"remarks\")\n\n        when (link.host) {\n            \"server\" -> {\n                bean as BrookBean\n                bean.protocol = \"\"\n\n                val server = link.queryParameter(\"server\")\n                    ?: error(\"Invalid brook server url (Missing server parameter): $text\")\n\n                bean.serverAddress = server.substringBefore(\":\")\n                bean.serverPort = server.substringAfter(\":\").toInt()\n                bean.password = link.queryParameter(\"password\")\n                    ?: error(\"Invalid brook server url (Missing password parameter): $text\")\n            }\n            \"wsserver\" -> {\n                bean as BrookBean\n                bean.protocol = \"ws\"\n\n\n                var wsserver = (link.queryParameter(\"wsserver\")\n                    ?: error(\"Invalid brook wsserver url (Missing wsserver parameter): $text\"))\n                    .substringAfter(\"://\")\n                if (wsserver.contains(\"/\")) {\n                    bean.wsPath = \"/\" + wsserver.substringAfter(\"/\")\n                    wsserver = wsserver.substringBefore(\"/\")\n                }\n                bean.serverAddress = wsserver.substringBefore(\":\")\n                bean.serverPort = wsserver.substringAfter(\":\").toInt()\n                bean.password = link.queryParameter(\"password\")\n                    ?: error(\"Invalid brook wsserver url (Missing password parameter): $text\")\n\n            }\n            \"wssserver\" -> {\n                bean as BrookBean\n                bean.protocol = \"wss\"\n\n\n                var wsserver = (link.queryParameter(\"wssserver\")\n                    ?: error(\"Invalid brook wssserver url (Missing wssserver parameter): $text\"))\n                    .substringAfter(\"://\")\n                if (wsserver.contains(\"/\")) {\n                    bean.wsPath = \"/\" + wsserver.substringAfter(\"/\")\n                    wsserver = wsserver.substringBefore(\"/\")\n                }\n                bean.serverAddress = wsserver.substringBefore(\":\")\n                bean.serverPort = wsserver.substringAfter(\":\").toInt()\n                bean.password = link.queryParameter(\"password\")\n                    ?: error(\"Invalid brook wssserver url (Missing password parameter): $text\")\n\n            }\n            \"socks5\" -> {\n                bean as SOCKSBean\n\n                val socks5 = (link.queryParameter(\"socks5\")\n                    ?: error(\"Invalid brook socks5 url (Missing socks5 parameter): $text\"))\n                    .substringAfter(\"://\")\n\n                bean.serverAddress = socks5.substringBefore(\":\")\n                bean.serverPort = socks5.substringAfter(\":\").toInt()\n\n                link.queryParameter(\"username\")?.also { username ->\n                    bean.username = username\n\n                    link.queryParameter(\"password\")?.also { password ->\n                        bean.password = password\n                    }\n                }\n            }\n        }\n\n        return bean\n\n    } else {\n\n        /**\n         * brook://urlEncode(password)@host:port#urlEncode(remarks)\n         * brook+ws(s)://urlEncode(password)@host:port?path=...#urlEncode(remarks)\n         */\n        val proto = if (!text.startsWith(\"brook+\")) \"\" else {\n            text.substringAfter(\"+\").substringBefore(\"://\")\n        }\n\n        if (proto !in arrayOf(\"\", \"ws\", \"wss\")) error(\"Invalid brook protocol $proto\")\n\n        val link = (\"https://\" + text.substringAfter(\"://\")).toHttpUrlOrNull()\n            ?: error(\"Invalid brook url: $text\")\n\n        return BrookBean().apply {\n            protocol = proto\n            serverAddress = link.host\n            serverPort = link.port\n            password = link.username\n            link.queryParameter(\"path\")?.also {\n                wsPath = it\n            }\n            name = link.fragment\n        }\n    }\n}\n\nfun BrookBean.toUri(): String {\n    /*var server = when (protocol) {\n        \"ws\" -> \"ws://$serverAddress:$serverPort\"\n        \"wss\" -> \"wss://$serverAddress:$serverPort\"\n        else -> \"$serverAddress:$serverPort\"\n    }\n    if (protocol.startsWith(\"ws\")) {\n        if (wsPath.isNotBlank()) {\n            if (!wsPath.startsWith(\"/\")) {\n                server += \"/\"\n            }\n            server += wsPath.pathSafe()\n        }\n    }\n    //if (name.isNotBlank()) {\n    //    server += \"#\" + name.urlSafe()\n    //}\n    if (password.isNotBlank()) {\n        server = \"$server $password\"\n    }\n    return \"brook://\" + server.urlSafe()*/\n\n    val builder = linkBuilder()\n        .host(serverAddress)\n        .port(serverPort)\n\n    if (password.isNotBlank()) {\n        builder.encodedUsername(password.urlSafe())\n    }\n\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n    if (wsPath.isNotBlank()) {\n        builder.addQueryParameter(\"path\", wsPath)\n    }\n\n    return when (protocol) {\n        \"ws\", \"wss\" -> builder.toLink(\"brook+$protocol\", false)\n        else -> builder.toLink(\"brook\")\n    }\n\n}\n\nfun BrookBean.internalUri(): String {\n    var server = wrapUri()\n    server = when (protocol) {\n        \"ws\" -> \"ws://\"\n        \"wss\" -> \"wss://\"\n        else -> return server\n    } + server\n    if (wsPath.isNotBlank()) {\n        if (!wsPath.startsWith(\"/\")) {\n            server += \"/\"\n        }\n        server += wsPath.pathSafe()\n    }\n    return server\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/GsonConverters.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\nimport androidx.room.TypeConverter;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\nimport cn.hutool.core.util.StrUtil;\nimport kotlin.collections.CollectionsKt;\nimport kotlin.collections.SetsKt;\n\npublic class GsonConverters {\n\n    @TypeConverter\n    public static String toJson(Object value) {\n        if (value instanceof Collection) {\n            if (((Collection<?>) value).isEmpty()) return \"\";\n        }\n        return GsonsKt.getGson().toJson(value);\n    }\n\n    @TypeConverter\n    public static List toList(String value) {\n        if (StrUtil.isBlank(value)) return CollectionsKt.listOf();\n        return GsonsKt.getGson().fromJson(value, List.class);\n    }\n\n    @TypeConverter\n    public static Set toSet(String value) {\n        if (StrUtil.isBlank(value)) return SetsKt.setOf();\n        return GsonsKt.getGson().fromJson(value, Set.class);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/Gsons.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson\n\nimport com.google.gson.GsonBuilder\n\nval gson = GsonBuilder()\n    .setPrettyPrinting()\n    .setLenient()\n    .registerTypeAdapterFactory(JsonOrAdapterFactory())\n    .registerTypeAdapterFactory(JsonLazyFactory())\n    .create()"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/JsonLazyAdapter.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonElement;\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonToken;\nimport com.google.gson.stream.JsonWriter;\n\nimport java.io.IOException;\n\npublic class JsonLazyAdapter<T> extends TypeAdapter<JsonLazyInterface<T>> {\n\n    private final Gson gson;\n    private final Class<JsonLazyInterface<T>> clazz;\n\n    public JsonLazyAdapter(Gson gson, Class<JsonLazyInterface<T>> clazz) {\n        this.gson = gson;\n        this.clazz = clazz;\n    }\n\n    @Override\n    public void write(JsonWriter out, JsonLazyInterface<T> value) throws IOException {\n        if (value == null) {\n            out.nullValue();\n        } else {\n            gson.getAdapter(value.type.getValue()).write(out, value.getValue());\n        }\n    }\n\n    @Override\n    public JsonLazyInterface<T> read(JsonReader in) throws IOException {\n        if (in.peek() == JsonToken.NULL) {\n            in.nextNull();\n            return null;\n        }\n\n        try {\n            JsonLazyInterface<T> instance = clazz.newInstance();\n            instance.gson = gson;\n            instance.content = gson.getAdapter(JsonElement.class).read(in);\n            return instance;\n        } catch (Exception e) {\n            if (e instanceof IOException) {\n                throw ((IOException) e);\n            } else {\n                throw new IOException(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/JsonLazyFactory.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\nimport com.google.gson.Gson;\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.TypeAdapterFactory;\nimport com.google.gson.reflect.TypeToken;\n\npublic class JsonLazyFactory implements TypeAdapterFactory {\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    @Override\n    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {\n        if (!JsonLazyInterface.class.isAssignableFrom(type.getRawType())) return null;\n        return new JsonLazyAdapter(gson, type.getRawType());\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/JsonLazyInterface.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonElement;\n\nimport kotlin.Lazy;\nimport kotlin.LazyKt;\n\n@SuppressWarnings(\"unchecked\")\npublic abstract class JsonLazyInterface<T> implements Lazy<T> {\n\n    protected JsonElement content;\n    protected Gson gson;\n    private T value;\n    private boolean fromValue;\n\n    public JsonLazyInterface() {\n    }\n\n    public JsonLazyInterface(T value) {\n        this.value = value;\n        this.fromValue = true;\n    }\n\n    protected final Lazy<Class<T>> type = LazyKt.lazy(() -> (Class<T>) getType());\n    private final Lazy<T> _value = LazyKt.lazy(this::init);\n\n    private T init() {\n        if (type.getValue() == null) {\n            return null;\n        }\n        return gson.fromJson(content, type.getValue());\n    }\n\n    @Nullable\n    protected abstract Class<? extends T> getType();\n\n    @Override\n    public T getValue() {\n        if (fromValue) return value;\n        return _value.getValue();\n    }\n\n    @Override\n    public boolean isInitialized() {\n        if (fromValue) return true;\n        return _value.isInitialized();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/JsonOr.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.gson.stream.JsonToken;\n\npublic class JsonOr<X, Y> {\n\n    public JsonToken tokenX;\n    public JsonToken tokenY;\n\n    public X valueX;\n    public Y valueY;\n\n    public JsonOr(JsonToken tokenX, JsonToken tokenY) {\n        this.tokenX = tokenX;\n        this.tokenY = tokenY;\n    }\n\n    protected JsonOr(X valueX, Y valueY) {\n        this.valueX = valueX;\n        this.valueY = valueY;\n    }\n\n    @NonNull\n    @Override\n    public String toString() {\n        return valueX != null ? valueX.toString() : valueY != null ? valueY.toString() : \"null\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/JsonOrAdapter.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\nimport com.google.gson.Gson;\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.reflect.TypeToken;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonToken;\nimport com.google.gson.stream.JsonWriter;\n\nimport java.io.IOException;\n\n@SuppressWarnings(\"unchecked\")\npublic class JsonOrAdapter<X, Y, T extends JsonOr<X, Y>> extends TypeAdapter<T> {\n\n    private final Gson gson;\n    private final TypeToken<X> typeX;\n    private final TypeToken<Y> typeY;\n    private final TypeToken<T> typeT;\n    private final JsonToken tokenX;\n\n    @SuppressWarnings(\"FieldCanBeLocal\")\n    private final JsonToken tokenY;\n\n    public JsonOrAdapter(Gson gson, TypeToken<X> typeX, TypeToken<Y> typeY, TypeToken<T> typeT, JsonToken tokenX, JsonToken tokenY) {\n        this.gson = gson;\n        this.typeX = typeX;\n        this.typeY = typeY;\n        this.typeT = typeT;\n        this.tokenX = tokenX;\n        this.tokenY = tokenY;\n    }\n\n    @Override\n    public void write(JsonWriter out, T value) throws IOException {\n        if (value.valueX != null) {\n            gson.getAdapter(typeX).write(out, value.valueX);\n        } else {\n            gson.getAdapter(typeY).write(out, value.valueY);\n        }\n    }\n\n    @Override\n    public T read(JsonReader in) throws IOException {\n        try {\n            T jsonOr = (T) typeT.getRawType().newInstance();\n            if (in.peek() == tokenX) {\n                jsonOr.valueX = gson.getAdapter(typeX).read(in);\n            } else {\n                jsonOr.valueY = gson.getAdapter(typeY).read(in);\n            }\n            return jsonOr;\n        } catch (Exception e) {\n            throw new IOException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/gson/JsonOrAdapterFactory.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.gson;\n\nimport com.google.gson.Gson;\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.TypeAdapterFactory;\nimport com.google.gson.reflect.TypeToken;\n\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\n\n@SuppressWarnings({\"ConstantConditions\", \"unchecked\", \"rawtypes\"})\npublic class JsonOrAdapterFactory implements TypeAdapterFactory {\n\n    @Override\n    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {\n        if (!JsonOr.class.isAssignableFrom(type.getRawType())) return null;\n        Type superclass = type.getRawType().getGenericSuperclass();\n        if (superclass instanceof Class) {\n            throw new RuntimeException(\"Missing type parameter.\");\n        }\n        ParameterizedType parameterized = (ParameterizedType) superclass;\n        Type[] args = parameterized.getActualTypeArguments();\n        try {\n            JsonOr<?, ?> instance = (JsonOr<?, ?>) type.getRawType().newInstance();\n            return new JsonOrAdapter(gson, TypeToken.get(args[0]), TypeToken.get(args[1]), type, instance.tokenX, instance.tokenY);\n        } catch (Exception e) {\n            e.printStackTrace();\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.http;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class HttpBean extends AbstractBean {\n\n    public String username;\n    public String password;\n    public boolean tls;\n    public String sni;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (username == null) username = \"\";\n        if (password == null) password = \"\";\n        if (sni == null) sni = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(username);\n        output.writeString(password);\n        output.writeBoolean(tls);\n        output.writeString(sni);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        username = input.readString();\n        password = input.readString();\n        tls = input.readBoolean();\n        sni = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public HttpBean clone() {\n        return KryoConverters.deserialize(new HttpBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<HttpBean> CREATOR = new CREATOR<HttpBean>() {\n        @NonNull\n        @Override\n        public HttpBean newInstance() {\n            return new HttpBean();\n        }\n\n        @Override\n        public HttpBean[] newArray(int size) {\n            return new HttpBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.http\n\nimport io.nekohasekai.sagernet.ktx.urlSafe\nimport okhttp3.HttpUrl\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nfun parseHttp(link: String): HttpBean {\n    val httpUrl = link.toHttpUrlOrNull() ?: error(\"Invalid http(s) link: $link\")\n\n    if (httpUrl.encodedPath != \"/\") error(\"Not http proxy\")\n\n    return HttpBean().apply {\n        serverAddress = httpUrl.host\n        serverPort = httpUrl.port\n        username = httpUrl.username\n        password = httpUrl.password\n        sni = httpUrl.queryParameter(\"sni\")\n        name = httpUrl.fragment\n        tls = httpUrl.scheme == \"https\"\n    }\n}\n\nfun HttpBean.toUri(): String {\n    val builder = HttpUrl.Builder()\n        .scheme(if (tls) \"https\" else \"http\")\n        .host(serverAddress)\n        .port(serverPort)\n\n    if (username.isNotBlank()) {\n        builder.username(username)\n    }\n    if (password.isNotBlank()) {\n        builder.password(password)\n    }\n    if (sni.isNotBlank()) {\n        builder.addQueryParameter(\"sni\", sni)\n    }\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n    return builder.toString()\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.hysteria;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class HysteriaBean extends AbstractBean {\n\n    public static final int TYPE_NONE = 0;\n    public static final int TYPE_STRING = 1;\n    public static final int TYPE_BASE64 = 2;\n\n    public Integer authPayloadType;\n    public String authPayload;\n\n    public String obfuscation;\n    public String sni;\n    public String caText;\n\n    public Integer uploadMbps;\n    public Integer downloadMbps;\n    public Boolean allowInsecure;\n    public Integer streamReceiveWindow;\n    public Integer connectionReceiveWindow;\n    public Boolean disableMtuDiscovery;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (authPayloadType == null) authPayloadType = TYPE_NONE;\n        if (authPayload == null) authPayload = \"\";\n        if (obfuscation == null) obfuscation = \"\";\n        if (sni == null) sni = \"\";\n        if (caText == null) caText = \"\";\n\n        if (uploadMbps == null) uploadMbps = 10;\n        if (downloadMbps == null) downloadMbps = 50;\n        if (allowInsecure == null) allowInsecure = false;\n\n        if (streamReceiveWindow == null) streamReceiveWindow = 0;\n        if (connectionReceiveWindow == null) connectionReceiveWindow = 0;\n        if (disableMtuDiscovery == null) disableMtuDiscovery = false;\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(1);\n        super.serialize(output);\n        output.writeInt(authPayloadType);\n        output.writeString(authPayload);\n        output.writeString(obfuscation);\n        output.writeString(sni);\n\n        output.writeInt(uploadMbps);\n        output.writeInt(downloadMbps);\n        output.writeBoolean(allowInsecure);\n\n        output.writeString(caText);\n        output.writeInt(streamReceiveWindow);\n        output.writeInt(connectionReceiveWindow);\n        output.writeBoolean(disableMtuDiscovery);\n\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        authPayloadType = input.readInt();\n        authPayload = input.readString();\n        obfuscation = input.readString();\n        sni = input.readString();\n        uploadMbps = input.readInt();\n        downloadMbps = input.readInt();\n        allowInsecure = input.readBoolean();\n        if (version >= 1) {\n            caText = input.readString();\n            streamReceiveWindow = input.readInt();\n            connectionReceiveWindow = input.readInt();\n            disableMtuDiscovery = input.readBoolean();\n        }\n    }\n\n    @Override\n    public void applyFeatureSettings(AbstractBean other) {\n        if (!(other instanceof HysteriaBean)) return;\n        HysteriaBean bean = ((HysteriaBean) other);\n        bean.uploadMbps = uploadMbps;\n        bean.downloadMbps = downloadMbps;\n        bean.allowInsecure = allowInsecure;\n        bean.disableMtuDiscovery = disableMtuDiscovery;\n    }\n\n    @NotNull\n    @Override\n    public HysteriaBean clone() {\n        return KryoConverters.deserialize(new HysteriaBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<HysteriaBean> CREATOR = new CREATOR<HysteriaBean>() {\n        @NonNull\n        @Override\n        public HysteriaBean newInstance() {\n            return new HysteriaBean();\n        }\n\n        @Override\n        public HysteriaBean[] newArray(int size) {\n            return new HysteriaBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.hysteria\n\nimport cn.hutool.core.util.NumberUtil\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.ktx.wrapUri\nimport java.io.File\n\nfun JSONObject.parseHysteria(): HysteriaBean {\n    return HysteriaBean().apply {\n        serverAddress = getStr(\"server\").substringBeforeLast(\":\")\n        serverPort = getStr(\"server\").substringAfterLast(\":\")\n            .takeIf { NumberUtil.isInteger(it) }\n            ?.toInt() ?: 443\n        uploadMbps = getInt(\"up_mbps\")\n        downloadMbps = getInt(\"down_mbps\")\n        obfuscation = getStr(\"obfs\")\n        getStr(\"auth\")?.also {\n            authPayloadType = HysteriaBean.TYPE_BASE64\n            authPayload = it\n        }\n        getStr(\"auth_str\")?.also {\n            authPayloadType = HysteriaBean.TYPE_STRING\n            authPayload = it\n        }\n        sni = getStr(\"server_name\")\n        allowInsecure = getBool(\"insecure\")\n\n        streamReceiveWindow = getInt(\"recv_window_conn\")\n        connectionReceiveWindow = getInt(\"recv_window\")\n        disableMtuDiscovery = getBool(\"disable_mtu_discovery\")\n    }\n}\n\nfun HysteriaBean.buildHysteriaConfig(port: Int, cacheFile: (() -> File)?): String {\n    return JSONObject().also {\n        it[\"server\"] = wrapUri()\n        it[\"up_mbps\"] = uploadMbps\n        it[\"down_mbps\"] = downloadMbps\n        it[\"socks5\"] = JSONObject(mapOf(\"listen\" to \"$LOCALHOST:$port\"))\n        it[\"obfs\"] = obfuscation\n        when (authPayloadType) {\n            HysteriaBean.TYPE_BASE64 -> it[\"auth\"] = authPayload\n            HysteriaBean.TYPE_STRING -> it[\"auth_str\"] = authPayload\n        }\n        if (sni.isNotBlank()) it[\"server_name\"] = sni\n        if (caText.isNotBlank() && cacheFile != null) {\n            val caFile = cacheFile()\n            caFile.writeText(caText)\n            it[\"ca\"] = caFile.absolutePath\n        }\n\n        if (allowInsecure) it[\"insecure\"] = true\n        if (streamReceiveWindow > 0) it[\"recv_window_conn\"] = streamReceiveWindow\n        if (connectionReceiveWindow > 0) it[\"recv_window\"] = connectionReceiveWindow\n        if (disableMtuDiscovery) it[\"disable_mtu_discovery\"] = true\n    }.toStringPretty()\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/internal/BalancerBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.internal;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class BalancerBean extends InternalBean {\n\n    public static final int TYPE_LIST = 0;\n    public static final int TYPE_GROUP = 1;\n\n    public Integer type;\n    public String strategy;\n    public List<Long> proxies;\n    public Long groupId;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (name == null) name = \"\";\n        if (strategy == null) strategy = \"\";\n        if (type == null) type = TYPE_LIST;\n        if (proxies == null) proxies = new ArrayList<>();\n        if (groupId == null) groupId = 0L;\n    }\n\n    @Override\n    public String displayName() {\n        if (StrUtil.isNotBlank(name)) {\n            return name;\n        } else {\n            return \"Balancer \" + Math.abs(hashCode());\n        }\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        output.writeInt(type);\n        output.writeString(strategy);\n        switch (type) {\n            case TYPE_LIST: {\n                int length = proxies.size();\n                output.writeInt(length);\n                for (Long proxy : proxies) {\n                    output.writeLong(proxy);\n                }\n                break;\n            }\n            case TYPE_GROUP: {\n                output.writeLong(groupId);\n                break;\n            }\n        }\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        type = input.readInt();\n        strategy = input.readString();\n        switch (type) {\n            case TYPE_LIST: {\n                int length = input.readInt();\n                proxies = new ArrayList<>();\n                for (int i = 0; i < length; i++) {\n                    proxies.add(input.readLong());\n                }\n                break;\n            }\n            case TYPE_GROUP: {\n                groupId = input.readLong();\n                break;\n            }\n        }\n    }\n\n    @NonNull\n    @Override\n    public BalancerBean clone() {\n        return KryoConverters.deserialize(new BalancerBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<BalancerBean> CREATOR = new CREATOR<BalancerBean>() {\n        @NonNull\n        @Override\n        public BalancerBean newInstance() {\n            return new BalancerBean();\n        }\n\n        @Override\n        public BalancerBean[] newArray(int size) {\n            return new BalancerBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/internal/ChainBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.internal;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class ChainBean extends InternalBean {\n\n    public List<Long> proxies;\n\n    @Override\n    public String displayName() {\n        if (StrUtil.isNotBlank(name)) {\n            return name;\n        } else {\n            return \"Chain \" + Math.abs(hashCode());\n        }\n    }\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (name == null) name = \"\";\n\n        if (proxies == null) {\n            proxies = new ArrayList<>();\n        }\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(1);\n        output.writeInt(proxies.size());\n        for (Long proxy : proxies) {\n            output.writeLong(proxy);\n        }\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        if (version < 1) {\n            input.readString();\n            input.readInt();\n        }\n        int length = input.readInt();\n        proxies = new ArrayList<>();\n        for (int i = 0; i < length; i++) {\n            proxies.add(input.readLong());\n        }\n    }\n\n    @NotNull\n    @Override\n    public ChainBean clone() {\n        return KryoConverters.deserialize(new ChainBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<ChainBean> CREATOR = new CREATOR<ChainBean>() {\n        @NonNull\n        @Override\n        public ChainBean newInstance() {\n            return new ChainBean();\n        }\n\n        @Override\n        public ChainBean[] newArray(int size) {\n            return new ChainBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/internal/ConfigBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.internal;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class ConfigBean extends InternalBean {\n\n    public String type;\n    public String content;\n\n    @Override\n    public String displayName() {\n        if (StrUtil.isNotBlank(name)) {\n            return name;\n        } else {\n            return \"Config \" + Math.abs(hashCode());\n        }\n    }\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (name == null) name = \"\";\n        if (type == null) type = \"v2ray\";\n        if (content == null) content = \"{}\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n\n        output.writeString(type);\n        output.writeString(content);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n\n        type = input.readString();\n        content = input.readString();\n    }\n\n    @NonNull\n    @Override\n    public ConfigBean clone() {\n        return KryoConverters.deserialize(new ConfigBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<ConfigBean> CREATOR = new CREATOR<ConfigBean>() {\n        @NonNull\n        @Override\n        public ConfigBean newInstance() {\n            return new ConfigBean();\n        }\n\n        @Override\n        public ConfigBean[] newArray(int size) {\n            return new ConfigBean[size];\n        }\n    };\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/internal/InternalBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.internal;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\n\npublic abstract class InternalBean extends AbstractBean {\n\n    @Override\n    public String displayAddress() {\n        return \"\";\n    }\n\n    @Override\n    public boolean canICMPing() {\n        return false;\n    }\n\n    @Override\n    public boolean canTCPing() {\n        return false;\n    }\n\n    @Override\n    public boolean canMapping() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.naive;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class NaiveBean extends AbstractBean {\n\n    /**\n     * Available proto: https, quic.\n     */\n    public String proto;\n    public String username;\n    public String password;\n    public String extraHeaders;\n\n    @Override\n    public void initializeDefaultValues() {\n        if (serverPort == null) serverPort = 443;\n        super.initializeDefaultValues();\n        if (proto == null) proto = \"https\";\n        if (username == null) username = \"\";\n        if (password == null) password = \"\";\n        if (extraHeaders == null) extraHeaders = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(proto);\n        output.writeString(username);\n        output.writeString(password);\n        output.writeString(extraHeaders);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        proto = input.readString();\n        username = input.readString();\n        password = input.readString();\n        extraHeaders = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public NaiveBean clone() {\n        return KryoConverters.deserialize(new NaiveBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<NaiveBean> CREATOR = new CREATOR<NaiveBean>() {\n        @NonNull\n        @Override\n        public NaiveBean newInstance() {\n            return new NaiveBean();\n        }\n\n        @Override\n        public NaiveBean[] newArray(int size) {\n            return new NaiveBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.naive\n\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.ktx.*\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nfun parseNaive(link: String): NaiveBean {\n    val proto = link.substringAfter(\"+\").substringBefore(\":\")\n    val url = (\"https://\" + link.substringAfter(\"://\")).toHttpUrlOrNull()\n        ?: error(\"Invalid naive link: $link\")\n    return NaiveBean().also {\n        it.proto = proto\n    }.apply {\n        serverAddress = url.host\n        serverPort = url.port\n        username = url.username\n        password = url.password\n        extraHeaders = url.queryParameter(\"extra-headers\")?.let {\n            it.unUrlSafe().replace(\"\\r\\n\", \"\\n\")\n        }\n        name = url.fragment\n        initializeDefaultValues()\n    }\n}\n\nfun NaiveBean.toUri(proxyOnly: Boolean = false): String {\n    val builder = linkBuilder().host(finalAddress).port(finalPort)\n    if (username.isNotBlank()) {\n        builder.username(username)\n        if (password.isNotBlank()) {\n            builder.password(password)\n        }\n    }\n    if (!proxyOnly) {\n        if (extraHeaders.isNotBlank()) {\n            builder.addQueryParameter(\"extra-headers\", extraHeaders)\n        }\n        if (name.isNotBlank()) {\n            builder.encodedFragment(name.urlSafe())\n        }\n    }\n    return builder.toLink(if (proxyOnly) proto else \"naive+$proto\", false)\n}\n\nfun NaiveBean.buildNaiveConfig(port: Int, mux: Boolean): String {\n    return JSONObject().also {\n        it[\"listen\"] = \"socks://$LOCALHOST:$port\"\n        it[\"proxy\"] = toUri(true)\n        if (extraHeaders.isNotBlank()) {\n            it[\"extra-headers\"] = extraHeaders.split(\"\\n\").joinToString(\"\\r\\n\")\n        }\n        if (!serverAddress.isIpAddress() && finalAddress == LOCALHOST) {\n            it[\"host-resolver-rules\"] = \"MAP $serverAddress $LOCALHOST\"\n        }\n        if (DataStore.enableLog) {\n            it[\"log\"] = \"\"\n        }\n        if (mux) {\n            it[\"concurrency\"] = DataStore.muxConcurrency\n        }\n    }.toStringPretty()\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/pingtunnel/PingTunnelBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.pingtunnel;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class PingTunnelBean extends AbstractBean {\n\n    public String key;\n\n    @Override\n    public String displayName() {\n        if (StrUtil.isNotBlank(name)) {\n            return name;\n        } else {\n            return serverAddress;\n        }\n    }\n\n    @Override\n    public boolean canTCPing() {\n        return false;\n    }\n\n    @Override\n    public boolean canMapping() {\n        return false;\n    }\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (key == null) key = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        output.writeString(serverAddress);\n        output.writeString(key);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        serverAddress = input.readString();\n        key = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public PingTunnelBean clone() {\n        return KryoConverters.deserialize(new PingTunnelBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<PingTunnelBean> CREATOR = new CREATOR<PingTunnelBean>() {\n        @NonNull\n        @Override\n        public PingTunnelBean newInstance() {\n            return new PingTunnelBean();\n        }\n\n        @Override\n        public PingTunnelBean[] newArray(int size) {\n            return new PingTunnelBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/pingtunnel/PingTunnelFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.pingtunnel\n\nimport io.nekohasekai.sagernet.ktx.linkBuilder\nimport io.nekohasekai.sagernet.ktx.toLink\nimport io.nekohasekai.sagernet.ktx.urlSafe\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\n\n/**\n * Unofficial\n *\n * ping-tunnel://[urlEncode(key)@]host[#urlEncode(remarks)]\n */\n\nfun parsePingTunnel(server: String): PingTunnelBean {\n    val link = server.replace(\"ping-tunnel://\", \"https://\").toHttpUrlOrNull()\n        ?: error(\"invalid PingTunnel link $server\")\n    return PingTunnelBean().apply {\n        serverAddress = link.host\n        key = link.username\n        link.fragment.takeIf { !it.isNullOrBlank() }?.let {\n            name = it\n        }\n        initializeDefaultValues()\n    }\n}\n\nfun PingTunnelBean.toUri(): String {\n    val builder = linkBuilder().host(serverAddress)\n    if (key.isNotBlank() && key != \"1\") {\n        builder.encodedUsername(key.urlSafe())\n    }\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n    return builder.toLink(\"ping-tunnel\", false)\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/relaybaton/RelayBatonBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.relaybaton;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class RelayBatonBean extends AbstractBean {\n\n    public String username;\n    public String password;\n\n    @Override\n    public void initializeDefaultValues() {\n        if (serverPort == null) serverPort = 443;\n        super.initializeDefaultValues();\n        if (username == null) username = \"\";\n        if (password == null) password = \"\";\n    }\n\n    @Override\n    public boolean canMapping() {\n        return false;\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        output.writeString(serverAddress);\n        output.writeString(username);\n        output.writeString(password);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        serverAddress = input.readString();\n        username = input.readString();\n        password = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public RelayBatonBean clone() {\n        return KryoConverters.deserialize(new RelayBatonBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<RelayBatonBean> CREATOR = new CREATOR<RelayBatonBean>() {\n        @NonNull\n        @Override\n        public RelayBatonBean newInstance() {\n            return new RelayBatonBean();\n        }\n\n        @Override\n        public RelayBatonBean[] newArray(int size) {\n            return new RelayBatonBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/relaybaton/RelayBatonFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.relaybaton\n\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.linkBuilder\nimport io.nekohasekai.sagernet.ktx.toLink\nimport io.nekohasekai.sagernet.ktx.urlSafe\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nfun parseRelayBaton(link: String): RelayBatonBean {\n    val url = (link.replace(\"relaybaton://\", \"https://\")).toHttpUrlOrNull()\n        ?: error(\"Invalid relaybaton link: $link\")\n    return RelayBatonBean().apply {\n        serverAddress = url.host\n        username = url.username\n        password = url.password\n        name = url.fragment\n        initializeDefaultValues()\n    }\n}\n\nfun RelayBatonBean.toUri(): String {\n    val builder = linkBuilder().host(serverAddress).username(username).password(password)\n\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n    return builder.toLink(\"relaybaton\", false)\n}\n\nfun RelayBatonBean.buildRelayBatonConfig(port: Int): String {\n    return \"\"\"\n        [client]\n        port = $port\n        http_port = 0\n        redir_port = 0\n        server = \"$finalAddress\"\n        username = \"$username\"\n        password = \"$password\"\n        proxy_all = true\n\n        [dns]\n        type = \"default\"\n       \n        [log]\n        file = \"stdout\"\n        level = \"${if (DataStore.enableLog) \"trace\" else \"error\"}\"\n    \"\"\".trimIndent()\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.shadowsocks;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class ShadowsocksBean extends AbstractBean {\n\n    public String method;\n    public String password;\n    public String plugin;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (StrUtil.isBlank(method)) method = \"aes-256-gcm\";\n        if (method == null) method = \"\";\n        if (password == null) password = \"\";\n        if (plugin == null) plugin = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(method);\n        output.writeString(password);\n        output.writeString(plugin);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        method = input.readString();\n        password = input.readString();\n        plugin = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public ShadowsocksBean clone() {\n        return KryoConverters.deserialize(new ShadowsocksBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<ShadowsocksBean> CREATOR = new CREATOR<ShadowsocksBean>() {\n        @NonNull\n        @Override\n        public ShadowsocksBean newInstance() {\n            return new ShadowsocksBean();\n        }\n\n        @Override\n        public ShadowsocksBean[] newArray(int size) {\n            return new ShadowsocksBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.shadowsocks\n\nimport cn.hutool.core.codec.Base64\nimport cn.hutool.json.JSONObject\nimport com.github.shadowsocks.plugin.PluginConfiguration\nimport com.github.shadowsocks.plugin.PluginManager\nimport com.github.shadowsocks.plugin.PluginOptions\nimport io.nekohasekai.sagernet.IPv6Mode\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.ktx.*\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nval methodsXray = arrayOf(\n    \"none\",\n    \"aes-128-gcm\",\n    \"aes-256-gcm\",\n    \"chacha20-ietf-poly1305\",\n    \"aes-256-cfb\",\n    \"aes-128-cfb\",\n    \"chacha20\",\n    \"chacha20-ietf\"\n)\n\nval methodsClash = arrayOf(\n    \"none\",\n    \"aes-128-gcm\",\n    \"aes-192-gcm\",\n    \"aes-256-gcm\",\n    \"chacha20-ietf-poly1305\",\n    \"xchacha20-ietf-poly1305\",\n    \"rc4\",\n    \"rc4-md5\",\n    \"aes-128-ctr\",\n    \"aes-192-ctr\",\n    \"aes-256-ctr\",\n    \"aes-128-cfb\",\n    \"aes-192-cfb\",\n    \"aes-256-cfb\",\n    \"aes-128-cfb8\",\n    \"aes-192-cfb8\",\n    \"aes-256-cfb8\",\n    \"aes-128-ofb\",\n    \"aes-192-ofb\",\n    \"aes-256-ofb\",\n    \"bf-cfb\",\n    \"cast5-cfb\",\n    \"des-cfb\",\n    \"idea-cfb\",\n    \"rc2-cfb\",\n    \"seed-cfb\",\n    \"camellia-128-cfb\",\n    \"camellia-192-cfb\",\n    \"camellia-256-cfb\",\n    \"camellia-128-cfb8\",\n    \"camellia-192-cfb8\",\n    \"camellia-256-cfb8\",\n    \"salsa20\",\n    \"chacha20\",\n    \"chacha20-ietf\",\n    \"xchacha20\",\n)\n\nval methodsSsRust = arrayOf(\n    \"none\",\n    \"rc4-md5\",\n    \"aes-128-cfb\",\n    \"aes-192-cfb\",\n    \"aes-256-cfb\",\n    \"aes-128-ctr\",\n    \"aes-192-ctr\",\n    \"aes-256-ctr\",\n    \"bf-cfb\",\n    \"camellia-128-cfb\",\n    \"camellia-192-cfb\",\n    \"camellia-256-cfb\",\n    \"chacha20-ietf\",\n    \"aes-128-gcm\",\n    \"aes-256-gcm\",\n    \"chacha20-ietf-poly1305\",\n    \"xchacha20-ietf-poly1305\",\n)\n\nval methodsSsLibev = arrayOf(\n    \"rc4-md5\",\n    \"aes-128-gcm\",\n    \"aes-192-gcm\",\n    \"aes-256-gcm\",\n    \"aes-128-cfb\",\n    \"aes-192-cfb\",\n    \"aes-256-cfb\",\n    \"aes-128-ctr\",\n    \"aes-192-ctr\",\n    \"aes-256-ctr\",\n    \"camellia-128-cfb\",\n    \"camellia-192-cfb\",\n    \"camellia-256-cfb\",\n    \"bf-cfb\",\n    \"chacha20-ietf-poly1305\",\n    \"xchacha20-ietf-poly1305\",\n    \"salsa20\",\n    \"chacha20\",\n    \"chacha20-ietf\"\n)\n\nfun PluginConfiguration.fixInvalidParams() {\n\n    if (selected.contains(\"v2ray\") && selected != \"v2ray-plugin\") {\n\n        pluginsOptions[\"v2ray-plugin\"] = getOptions().apply { id = \"v2ray-plugin\" }\n        pluginsOptions.remove(selected)\n        selected = \"v2ray-plugin\"\n\n        // resolve v2ray plugin\n\n    }\n\n    if (selected.contains(\"obfs\") && selected != \"obfs-local\") {\n\n        pluginsOptions[\"obfs-local\"] = getOptions().apply { id = \"obfs-local\" }\n        pluginsOptions.remove(selected)\n        selected = \"obfs-local\"\n\n        // resolve clash obfs\n\n    }\n\n    if (selected == \"obfs-local\") {\n        val options = pluginsOptions[\"obfs-local\"]\n        if (options != null) {\n            if (options.containsKey(\"mode\")) {\n                options[\"obfs\"] = options[\"mode\"]\n                options.remove(\"mode\")\n            }\n            if (options.containsKey(\"host\")) {\n                options[\"obfs-host\"] = options[\"host\"]\n                options.remove(\"host\")\n            }\n        }\n    }\n\n}\n\nfun ShadowsocksBean.fixInvalidParams() {\n    if (method == \"plain\") method = \"none\"\n    plugin = PluginConfiguration(plugin).apply { fixInvalidParams() }.toString()\n\n}\n\nfun parseShadowsocks(url: String): ShadowsocksBean {\n\n    if (url.contains(\"@\")) {\n\n        var link = url.replace(\"ss://\", \"https://\").toHttpUrlOrNull() ?: error(\n            \"invalid ss-android link $url\"\n        )\n\n        if (link.username.isBlank()) { // fix justmysocks's shit link\n\n            link = ((\"https://\" + url.substringAfter(\"ss://\")\n                .substringBefore(\"#\")\n                .decodeBase64UrlSafe()).toHttpUrlOrNull() ?: error(\n                \"invalid jms link $url\"\n            )).newBuilder().fragment(url.substringAfter(\"#\")).build()\n        }\n\n        // ss-android style\n\n        if (link.password.isNotBlank()) {\n\n            return ShadowsocksBean().apply {\n\n                serverAddress = link.host\n                serverPort = link.port\n                method = link.username\n                password = link.password\n                plugin = link.queryParameter(\"plugin\") ?: \"\"\n                name = link.fragment ?: \"\"\n\n                fixInvalidParams()\n\n            }\n\n        }\n\n        val methodAndPswd = link.username.decodeBase64UrlSafe()\n\n        return ShadowsocksBean().apply {\n\n            serverAddress = link.host\n            serverPort = link.port\n            method = methodAndPswd.substringBefore(\":\")\n            password = methodAndPswd.substringAfter(\":\")\n            plugin = link.queryParameter(\"plugin\") ?: \"\"\n            name = link.fragment ?: \"\"\n\n            fixInvalidParams()\n\n        }\n\n    } else {\n\n        // v2rayN style\n\n        var v2Url = url\n\n        if (v2Url.contains(\"#\")) v2Url = v2Url.substringBefore(\"#\")\n\n        val link = (\"https://\" + v2Url.substringAfter(\"ss://\")\n            .decodeBase64UrlSafe()).toHttpUrlOrNull() ?: error(\"invalid v2rayN link $url\")\n\n        return ShadowsocksBean().apply {\n\n            serverAddress = link.host\n            serverPort = link.port\n            method = link.username\n            password = link.password\n            plugin = \"\"\n            if (url.contains(\"#\")) {\n                name = url.substringAfter(\"#\").unUrlSafe()\n            }\n\n            fixInvalidParams()\n\n        }\n\n    }\n\n}\n\nfun ShadowsocksBean.toUri(): String {\n\n    val builder = linkBuilder().username(Base64.encodeUrlSafe(\"$method:$password\"))\n        .host(serverAddress)\n        .port(serverPort)\n\n    if (plugin.isNotBlank()) {\n        builder.addQueryParameter(\"plugin\", plugin)\n    }\n\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n    return builder.toLink(\"ss\").replace(\"$serverPort/\", \"$serverPort\")\n\n}\n\nfun JSONObject.parseShadowsocks(): ShadowsocksBean {\n    return ShadowsocksBean().apply {\n        var pluginStr = \"\"\n        val pId = getStr(\"plugin\")\n        if (!pId.isNullOrBlank()) {\n            val plugin = PluginOptions(pId, getStr(\"plugin_opts\"))\n            pluginStr = plugin.toString(false)\n        }\n\n        serverAddress = getStr(\"server\")\n        serverPort = getInt(\"server_port\")\n        password = getStr(\"password\")\n        method = getStr(\"method\")\n        plugin = pluginStr\n        name = getStr(\"remarks\", \"\")\n\n        fixInvalidParams()\n    }\n}\n\n\nfun ShadowsocksBean.buildShadowsocksConfig(port: Int): String {\n    val proxyConfig = JSONObject().also {\n        it[\"server\"] = finalAddress\n        it[\"server_port\"] = finalPort\n        it[\"method\"] = method\n        it[\"password\"] = password\n        it[\"local_address\"] = LOCALHOST\n        it[\"local_port\"] = port\n        it[\"local_udp_address\"] = LOCALHOST\n        it[\"local_udp_port\"] = port\n        it[\"mode\"] = \"tcp_and_udp\"\n        it[\"ipv6_first\"] = DataStore.ipv6Mode >= IPv6Mode.PREFER\n        it[\"keep_alive\"] = DataStore.tcpKeepAliveInterval\n    }\n\n    if (plugin.isNotBlank()) {\n        val pluginConfiguration = PluginConfiguration(plugin ?: \"\")\n        PluginManager.init(pluginConfiguration)?.let { (path, opts, _) ->\n            proxyConfig[\"plugin\"] = path\n            proxyConfig[\"plugin_opts\"] = opts.toString()\n        }\n    }\n\n    return proxyConfig.toStringPretty()\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocksr/ShadowsocksRBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.shadowsocksr;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class ShadowsocksRBean extends AbstractBean {\n\n    public String password;\n    public String method;\n    public String protocol;\n    public String protocolParam;\n    public String obfs;\n    public String obfsParam;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (password == null) password = \"\";\n        if (StrUtil.isBlank(method)) method = \"aes-256-cfb\";\n        if (StrUtil.isBlank(protocol)) protocol = \"origin\";\n        if (protocolParam == null) protocolParam = \"\";\n        if (StrUtil.isBlank(obfs)) obfs = \"plain\";\n        if (obfsParam == null) obfsParam = \"\";\n\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(password);\n        output.writeString(method);\n        output.writeString(protocol);\n        output.writeString(protocolParam);\n        output.writeString(obfs);\n        output.writeString(obfsParam);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        password = input.readString();\n        method = input.readString();\n        protocol = input.readString();\n        protocolParam = input.readString();\n        obfs = input.readString();\n        obfsParam = input.readString();\n\n    }\n\n    @NotNull\n    @Override\n    public ShadowsocksRBean clone() {\n        return KryoConverters.deserialize(new ShadowsocksRBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<ShadowsocksRBean> CREATOR = new CREATOR<ShadowsocksRBean>() {\n        @NonNull\n        @Override\n        public ShadowsocksRBean newInstance() {\n            return new ShadowsocksRBean();\n        }\n\n        @Override\n        public ShadowsocksRBean[] newArray(int size) {\n            return new ShadowsocksRBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocksr/ShadowsocksRFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.shadowsocksr\n\nimport cn.hutool.core.codec.Base64\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.IPv6Mode\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport io.nekohasekai.sagernet.ktx.decodeBase64UrlSafe\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport java.util.*\n\nfun parseShadowsocksR(url: String): ShadowsocksRBean {\n\n    val params = url.substringAfter(\"ssr://\").decodeBase64UrlSafe().split(\":\")\n\n    val bean = ShadowsocksRBean().apply {\n        serverAddress = params[0]\n        serverPort = params[1].toInt()\n        protocol = params[2]\n        method = params[3]\n        obfs = params[4]\n        password = params[5].substringBefore(\"/\").decodeBase64UrlSafe()\n    }\n\n    val httpUrl = (\"https://localhost\" + params[5].substringAfter(\"/\")).toHttpUrl()\n\n    runCatching {\n        bean.obfsParam = httpUrl.queryParameter(\"obfsparam\")!!.decodeBase64UrlSafe()\n    }\n    runCatching {\n        bean.protocolParam = httpUrl.queryParameter(\"protoparam\")!!.decodeBase64UrlSafe()\n    }\n\n    val remarks = httpUrl.queryParameter(\"remarks\")\n    if (!remarks.isNullOrBlank()) {\n        bean.name = remarks.decodeBase64UrlSafe()\n    }\n\n    return bean\n\n}\n\nfun ShadowsocksRBean.toUri(): String {\n\n    return \"ssr://\" + Base64.encodeUrlSafe(\n        \"%s:%d:%s:%s:%s:%s/?obfsparam=%s&protoparam=%s&remarks=%s\".format(\n            Locale.ENGLISH, serverAddress, serverPort, protocol, method, obfs, Base64.encodeUrlSafe(\"%s\".format(Locale.ENGLISH, password)), Base64.encodeUrlSafe(\"%s\".format(Locale.ENGLISH, obfsParam)), Base64.encodeUrlSafe(\"%s\".format(Locale.ENGLISH, protocolParam)), Base64.encodeUrlSafe(\n                \"%s\".format(\n                    Locale.ENGLISH, name ?: \"\"\n                )\n            )\n        )\n    )\n}\n\nfun ShadowsocksRBean.buildShadowsocksRConfig(): String {\n    return JSONObject().also {\n        it[\"server\"] = finalAddress\n        it[\"server_port\"] = finalPort\n        it[\"method\"] = method\n        it[\"password\"] = password\n        it[\"protocol\"] = protocol\n        it[\"protocol_param\"] = protocolParam\n        it[\"obfs\"] = obfs\n        it[\"obfs_param\"] = obfsParam\n        it[\"ipv6\"] = DataStore.ipv6Mode >= IPv6Mode.ENABLE\n    }.toStringPretty()\n}\n\nfun JSONObject.parseShadowsocksR(): ShadowsocksRBean {\n    return ShadowsocksRBean().applyDefaultValues().apply {\n        serverAddress = getStr(\"server\", serverAddress)\n        serverPort = getInt(\"server_port\", serverPort)\n        method = getStr(\"method\", method)\n        password = getStr(\"password\", password)\n        protocol = getStr(\"protocol\", protocol)\n        protocolParam = getStr(\"protocol_param\", protocolParam)\n        obfs = getStr(\"obfs\", obfs)\n        obfsParam = getStr(\"obfs_param\", obfsParam)\n        name = getStr(\"remarks\", name)\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.snell;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class SnellBean extends AbstractBean {\n\n    public Integer version;\n    public String psk;\n    public String obfsMode;\n    public String obfsHost;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (version == null) version = 2;\n        if (psk == null) psk = \"\";\n        if (obfsMode == null) obfsMode = \"http\";\n        if (obfsHost == null) obfsHost = \"bing.com\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeInt(version);\n        output.writeString(psk);\n        output.writeString(obfsMode);\n        output.writeString(obfsHost);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int serVer = input.readInt();\n        super.deserialize(input);\n        version = input.readInt();\n        psk = input.readString();\n        obfsMode = input.readString();\n        obfsHost = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public SnellBean clone() {\n        return KryoConverters.deserialize(new SnellBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<SnellBean> CREATOR = new CREATOR<SnellBean>() {\n        @NonNull\n        @Override\n        public SnellBean newInstance() {\n            return new SnellBean();\n        }\n\n        @Override\n        public SnellBean[] newArray(int size) {\n            return new SnellBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.socks;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class SOCKSBean extends AbstractBean {\n\n    public Integer protocol;\n\n    public int protocolVersion() {\n        switch (protocol) {\n            case 0:\n            case 1:\n                return 4;\n            default:\n                return 5;\n        }\n    }\n\n    public String protocolName() {\n        switch (protocol) {\n            case 0:\n                return \"SOCKS4\";\n            case 1:\n                return \"SOCKS4A\";\n            default:\n                return \"SOCKS5\";\n        }\n    }\n\n    public String protocolVersionName() {\n        switch (protocol) {\n            case 0:\n                return \"4\";\n            case 1:\n                return \"4a\";\n            default:\n                return \"5\";\n        }\n    }\n\n    public String username;\n    public String password;\n    public boolean tls;\n    public String sni;\n\n    public static final int PROTOCOL_SOCKS4 = 0;\n    public static final int PROTOCOL_SOCKS4A = 1;\n    public static final int PROTOCOL_SOCKS5 = 2;\n\n    @Override\n    public String network() {\n        if (protocol < PROTOCOL_SOCKS5) return \"tcp\";\n        return super.network();\n    }\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (protocol == null) protocol = PROTOCOL_SOCKS5;\n        if (username == null) username = \"\";\n        if (password == null) password = \"\";\n        if (sni == null) sni = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(1);\n        super.serialize(output);\n        output.writeInt(protocol);\n        output.writeString(username);\n        output.writeString(password);\n        output.writeBoolean(tls);\n        output.writeString(sni);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        if (version >= 1) {\n            protocol = input.readInt();\n        }\n        username = input.readString();\n        password = input.readString();\n        tls = input.readBoolean();\n        sni = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public SOCKSBean clone() {\n        return KryoConverters.deserialize(new SOCKSBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<SOCKSBean> CREATOR = new CREATOR<SOCKSBean>() {\n        @NonNull\n        @Override\n        public SOCKSBean newInstance() {\n            return new SOCKSBean();\n        }\n\n        @Override\n        public SOCKSBean[] newArray(int size) {\n            return new SOCKSBean[size];\n        }\n    };\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.socks\n\nimport cn.hutool.core.codec.Base64\nimport io.nekohasekai.sagernet.ktx.decodeBase64UrlSafe\nimport io.nekohasekai.sagernet.ktx.toLink\nimport io.nekohasekai.sagernet.ktx.unUrlSafe\nimport io.nekohasekai.sagernet.ktx.urlSafe\nimport okhttp3.HttpUrl\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nfun parseSOCKS(link: String): SOCKSBean {\n    if (!link.substringAfter(\"://\").contains(\":\")) {\n        // v2rayN shit format\n        var url = link.substringAfter(\"://\")\n        if (url.contains(\"#\")) {\n            url = url.substringBeforeLast(\"#\")\n        }\n        url = url.decodeBase64UrlSafe()\n        val httpUrl = \"http://$url\".toHttpUrlOrNull() ?: error(\"Invalid v2rayN link content: $url\")\n        return SOCKSBean().apply {\n            serverAddress = httpUrl.host\n            serverPort = httpUrl.port\n            username = httpUrl.username.takeIf { it != \"null\" } ?: \"\"\n            password = httpUrl.password.takeIf { it != \"null\" } ?: \"\"\n            if (link.contains(\"#\")) {\n                name = link.substringAfter(\"#\").unUrlSafe()\n            }\n        }\n    } else {\n        val url = (\"http://\" + link.substringAfter(\"://\")).toHttpUrlOrNull()\n            ?: error(\"Not supported: $link\")\n\n        return SOCKSBean().apply {\n            protocol = when {\n                link.startsWith(\"socks4://\") -> SOCKSBean.PROTOCOL_SOCKS4\n                link.startsWith(\"socks4a://\") -> SOCKSBean.PROTOCOL_SOCKS4A\n                else -> SOCKSBean.PROTOCOL_SOCKS5\n            }\n            serverAddress = url.host\n            serverPort = url.port\n            username = url.username\n            password = url.password\n            name = url.fragment\n            tls = url.queryParameter(\"tls\") == \"true\"\n            sni = url.queryParameter(\"sni\")\n        }\n    }\n}\n\nfun SOCKSBean.toUri(): String {\n\n    val builder = HttpUrl.Builder().scheme(\"http\").host(serverAddress).port(serverPort)\n    if (!username.isNullOrBlank()) builder.username(username)\n    if (!password.isNullOrBlank()) builder.password(password)\n    if (tls) {\n        builder.addQueryParameter(\"tls\", \"true\")\n        if (sni.isNotBlank()) {\n            builder.addQueryParameter(\"sni\", sni)\n        }\n    }\n    if (!name.isNullOrBlank()) builder.encodedFragment(name.urlSafe())\n    return builder.toLink(\"socks${protocolVersion()}\")\n\n}\n\nfun SOCKSBean.toV2rayN(): String {\n\n    var link = \"\"\n    if (username.isNotBlank()) {\n        link += username.urlSafe() + \":\" + password.urlSafe() + \"@\"\n    }\n    link += \"$serverAddress:$serverPort\"\n    link = \"socks://\" + Base64.encode(link)\n    if (name.isNotBlank()) {\n        link += \"#\" + name.urlSafe()\n    }\n\n    return link\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHBean.java",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.ssh;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class SSHBean extends AbstractBean {\n\n    public static final int AUTH_TYPE_NONE = 0;\n    public static final int AUTH_TYPE_PASSWORD = 1;\n    public static final int AUTH_TYPE_PRIVATE_KEY = 2;\n\n    public String username;\n    public Integer authType;\n    public String password;\n    public String privateKey;\n    public String privateKeyPassphrase;\n    public String publicKey;\n\n    @Override\n    public void initializeDefaultValues() {\n        if (serverPort == null) serverPort = 22;\n\n        super.initializeDefaultValues();\n\n        if (username == null) username = \"root\";\n        if (authType == null) authType = AUTH_TYPE_PASSWORD;\n        if (password == null) password = \"\";\n        if (privateKey == null) privateKey = \"\";\n        if (privateKeyPassphrase == null) privateKeyPassphrase = \"\";\n        if (publicKey == null) publicKey = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(username);\n        output.writeInt(authType);\n        switch (authType) {\n            case AUTH_TYPE_NONE:\n                break;\n            case AUTH_TYPE_PASSWORD:\n                output.writeString(password);\n                break;\n            case AUTH_TYPE_PRIVATE_KEY:\n                output.writeString(privateKey);\n                output.writeString(privateKeyPassphrase);\n                break;\n        }\n        output.writeString(publicKey);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        username = input.readString();\n        authType = input.readInt();\n        switch (authType) {\n            case AUTH_TYPE_NONE:\n                break;\n            case AUTH_TYPE_PASSWORD:\n                password = input.readString();\n                break;\n            case AUTH_TYPE_PRIVATE_KEY:\n                privateKey = input.readString();\n                privateKeyPassphrase = input.readString();\n                break;\n        }\n        publicKey = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public SSHBean clone() {\n        return KryoConverters.deserialize(new SSHBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<SSHBean> CREATOR = new CREATOR<SSHBean>() {\n        @NonNull\n        @Override\n        public SSHBean newInstance() {\n            return new SSHBean();\n        }\n\n        @Override\n        public SSHBean[] newArray(int size) {\n            return new SSHBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.trojan;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class TrojanBean extends AbstractBean {\n\n    public String password;\n\n    public String security;\n    public String sni;\n    public String alpn;\n    public String flow;\n\n    // --------------------------------------- //\n\n    public Boolean allowInsecure;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (password == null) password = \"\";\n        if (StrUtil.isBlank(security)) security = \"tls\";\n        if (sni == null) sni = \"\";\n        if (alpn == null) alpn = \"\";\n        if (flow == null) flow = \"\";\n        if (allowInsecure == null) allowInsecure = false;\n\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(1);\n        super.serialize(output);\n        output.writeString(password);\n        output.writeString(security);\n        output.writeString(sni);\n        output.writeString(alpn);\n\n        if (\"xtls\".equals(security)) {\n            output.writeString(flow);\n        }\n\n        output.writeBoolean(allowInsecure);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        password = input.readString();\n        security = input.readString();\n        sni = input.readString();\n        alpn = input.readString();\n\n        if (\"xtls\".equals(security)) {\n            flow = input.readString();\n        }\n\n        if (version >= 1) {\n            allowInsecure = input.readBoolean();\n        }\n    }\n\n    @Override\n    public void applyFeatureSettings(AbstractBean other) {\n        if (!(other instanceof TrojanBean)) return;\n        TrojanBean bean = ((TrojanBean) other);\n        bean.allowInsecure = allowInsecure;\n    }\n\n    @NotNull\n    @Override\n    public TrojanBean clone() {\n        return KryoConverters.deserialize(new TrojanBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<TrojanBean> CREATOR = new CREATOR<TrojanBean>() {\n        @NonNull\n        @Override\n        public TrojanBean newInstance() {\n            return new TrojanBean();\n        }\n\n        @Override\n        public TrojanBean[] newArray(int size) {\n            return new TrojanBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.trojan\n\nimport cn.hutool.json.JSONArray\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.IPv6Mode\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.ktx.isIpAddress\nimport io.nekohasekai.sagernet.ktx.linkBuilder\nimport io.nekohasekai.sagernet.ktx.toLink\nimport io.nekohasekai.sagernet.ktx.urlSafe\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\n// WTF\n// https://github.com/trojan-gfw/igniter/issues/318\nfun parseTrojan(server: String): TrojanBean {\n\n    val link = server.replace(\"trojan://\", \"https://\").toHttpUrlOrNull()\n        ?: error(\"invalid trojan link $server\")\n\n    return TrojanBean().apply {\n        serverAddress = link.host\n        serverPort = link.port\n        password = link.username\n\n        if (link.password.isNotBlank()) {\n            password += \":\" + link.password\n        }\n\n        security = link.queryParameter(\"security\") ?: \"tls\"\n        sni = link.queryParameter(\"sni\") ?: \"\"\n        alpn = link.queryParameter(\"alpn\") ?: \"\"\n        flow = link.queryParameter(\"flow\") ?: \"\"\n        name = link.fragment ?: \"\"\n    }\n\n}\n\nfun TrojanBean.toUri(): String {\n\n    val builder = linkBuilder().username(password).host(serverAddress).port(serverPort)\n\n    if (sni.isNotBlank()) {\n        builder.addQueryParameter(\"sni\", sni)\n    }\n    if (alpn.isNotBlank()) {\n        builder.addQueryParameter(\"alpn\", alpn)\n    }\n\n    when (security) {\n        \"tls\" -> {\n        }\n        \"xtls\" -> {\n            builder.addQueryParameter(\"security\", security)\n            builder.addQueryParameter(\"flow\", flow)\n        }\n    }\n\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n\n    return builder.toLink(\"trojan\")\n\n}\n\nfun TrojanBean.buildTrojanConfig(port: Int): String {\n    return JSONObject().also { conf ->\n        conf[\"run_type\"] = \"client\"\n        conf[\"local_addr\"] = LOCALHOST\n        conf[\"local_port\"] = port\n        conf[\"remote_addr\"] = finalAddress\n        conf[\"remote_port\"] = finalPort\n        conf[\"password\"] = JSONArray().apply {\n            add(password)\n        }\n        conf[\"log_level\"] = if (DataStore.enableLog) 0 else 2\n\n        conf[\"ssl\"] = JSONObject().also {\n            if (allowInsecure) it[\"verify\"] = false\n            if (sni.isBlank() && finalAddress == LOCALHOST && !serverAddress.isIpAddress()) {\n                sni = serverAddress\n            }\n            if (sni.isNotBlank()) it[\"sni\"] = sni\n            if (alpn.isNotBlank()) it[\"alpn\"] = JSONArray(alpn.split(\"\\n\"))\n        }\n    }.toStringPretty()\n}\n\nfun TrojanBean.buildTrojanGoConfig(port: Int, mux: Boolean): String {\n    return JSONObject().also { conf ->\n        conf[\"run_type\"] = \"client\"\n        conf[\"local_addr\"] = LOCALHOST\n        conf[\"local_port\"] = port\n        conf[\"remote_addr\"] = finalAddress\n        conf[\"remote_port\"] = finalPort\n        conf[\"password\"] = JSONArray().apply {\n            add(password)\n        }\n        conf[\"log_level\"] = if (DataStore.enableLog) 0 else 2\n        if (mux && DataStore.enableMuxForAll) conf[\"mux\"] = JSONObject().also {\n            it[\"enabled\"] = true\n            it[\"concurrency\"] = DataStore.muxConcurrency\n        }\n        conf[\"tcp\"] = JSONObject().also {\n            it[\"prefer_ipv4\"] = DataStore.ipv6Mode <= IPv6Mode.ENABLE\n        }\n\n        conf[\"ssl\"] = JSONObject().also {\n            if (allowInsecure) it[\"verify\"] = false\n            if (sni.isBlank() && finalAddress == LOCALHOST && !serverAddress.isIpAddress()) {\n                sni = serverAddress\n            }\n            if (sni.isNotBlank()) it[\"sni\"] = sni\n            if (alpn.isNotBlank()) it[\"alpn\"] = JSONArray(alpn.split(\"\\n\"))\n        }\n    }.toStringPretty()\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.trojan_go;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class TrojanGoBean extends AbstractBean {\n\n    /**\n     * Trojan 的密码。\n     * 不可省略，不能为空字符串，不建议含有非 ASCII 可打印字符。\n     * 必须使用 encodeURIComponent 编码。\n     */\n    public String password;\n\n    /**\n     * 自定义 TLS 的 SNI。\n     * 省略时默认与 trojan-host 同值。不得为空字符串。\n     * <p>\n     * 必须使用 encodeURIComponent 编码。\n     */\n    public String sni;\n\n    /**\n     * 传输类型。\n     * 省略时默认为 original，但不可为空字符串。\n     * 目前可选值只有 original 和 ws，未来可能会有 h2、h2+ws 等取值。\n     * <p>\n     * 当取值为 original 时，使用原始 Trojan 传输方式，无法方便通过 CDN。\n     * 当取值为 ws 时，使用 wss 作为传输层。\n     */\n    public String type;\n\n    /**\n     * 自定义 HTTP Host 头。\n     * 可以省略，省略时值同 trojan-host。\n     * 可以为空字符串，但可能带来非预期情形。\n     * <p>\n     * 警告：若你的端口非标准端口（不是 80 / 443），RFC 标准规定 Host 应在主机名后附上端口号，例如 example.com:44333。至于是否遵守，请自行斟酌。\n     * <p>\n     * 必须使用 encodeURIComponent 编码。\n     */\n    public String host;\n\n    /**\n     * 当传输类型 type 取 ws、h2、h2+ws 时，此项有效。\n     * 不可省略，不可为空。\n     * 必须以 / 开头。\n     * 可以使用 URL 中的 & # ? 等字符，但应当是合法的 URL 路径。\n     * <p>\n     * 必须使用 encodeURIComponent 编码。\n     */\n    public String path;\n\n    /**\n     * 用于保证 Trojan 流量密码学安全的加密层。\n     * 可省略，默认为 none，即不使用加密。\n     * 不可以为空字符串。\n     * <p>\n     * 必须使用 encodeURIComponent 编码。\n     * <p>\n     * 使用 Shadowsocks 算法进行流量加密时，其格式为：\n     * <p>\n     * ss;method:password\n     * <p>\n     * 其中 ss 是固定内容，method 是加密方法，必须为下列之一：\n     * <p>\n     * aes-128-gcm\n     * aes-256-gcm\n     * chacha20-ietf-poly1305\n     */\n    public String encryption;\n\n    /**\n     * 额外的插件选项。本字段保留。\n     * 可省略，但不可以为空字符串。\n     */\n    public String plugin;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (password == null) password = \"\";\n        if (sni == null) sni = \"\";\n        if (StrUtil.isBlank(type)) type = \"original\";\n        if (host == null) host = \"\";\n        if (path == null) path = \"\";\n        if (StrUtil.isBlank(encryption)) encryption = \"none\";\n        if (plugin == null) plugin = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(password);\n        output.writeString(sni);\n        output.writeString(type);\n        //noinspection SwitchStatementWithTooFewBranches\n        switch (type) {\n            case \"ws\": {\n                output.writeString(host);\n                output.writeString(path);\n                break;\n            }\n        }\n        output.writeString(encryption);\n        output.writeString(plugin);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n\n        password = input.readString();\n        sni = input.readString();\n        type = input.readString();\n        //noinspection SwitchStatementWithTooFewBranches\n        switch (type) {\n            case \"ws\": {\n                host = input.readString();\n                path = input.readString();\n                break;\n            }\n        }\n        encryption = input.readString();\n        plugin = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public TrojanGoBean clone() {\n        return KryoConverters.deserialize(new TrojanGoBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<TrojanGoBean> CREATOR = new CREATOR<TrojanGoBean>() {\n        @NonNull\n        @Override\n        public TrojanGoBean newInstance() {\n            return new TrojanGoBean();\n        }\n\n        @Override\n        public TrojanGoBean[] newArray(int size) {\n            return new TrojanGoBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.trojan_go\n\nimport cn.hutool.json.JSONArray\nimport cn.hutool.json.JSONObject\nimport com.github.shadowsocks.plugin.PluginConfiguration\nimport com.github.shadowsocks.plugin.PluginManager\nimport com.github.shadowsocks.plugin.PluginOptions\nimport io.nekohasekai.sagernet.IPv6Mode\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams\nimport io.nekohasekai.sagernet.ktx.*\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\n\nfun parseTrojanGo(server: String): TrojanGoBean {\n    val link = server.replace(\"trojan-go://\", \"https://\").toHttpUrlOrNull() ?: error(\n        \"invalid trojan-link link $server\"\n    )\n    return TrojanGoBean().apply {\n        serverAddress = link.host\n        serverPort = link.port\n        password = link.username\n        link.queryParameter(\"sni\")?.let {\n            sni = it\n        }\n        link.queryParameter(\"type\")?.let { lType ->\n            type = lType\n\n            when (type) {\n                \"ws\" -> {\n                    link.queryParameter(\"host\")?.let {\n                        host = it\n                    }\n                    link.queryParameter(\"path\")?.let {\n                        path = it\n                    }\n                }\n                else -> {\n                }\n            }\n        }\n        link.queryParameter(\"encryption\")?.let {\n            encryption = it\n        }\n        link.queryParameter(\"plugin\")?.let {\n            plugin = it\n        }\n        link.fragment.takeIf { !it.isNullOrBlank() }?.let {\n            name = it\n        }\n    }\n}\n\nfun TrojanGoBean.toUri(): String {\n    val builder = linkBuilder().username(password).host(serverAddress).port(serverPort)\n    if (sni.isNotBlank()) {\n        builder.addQueryParameter(\"sni\", sni)\n    }\n    if (type.isNotBlank() && type != \"original\") {\n        builder.addQueryParameter(\"type\", type)\n\n        when (type) {\n            \"ws\" -> {\n                if (host.isNotBlank()) {\n                    builder.addQueryParameter(\"host\", host)\n                }\n                if (path.isNotBlank()) {\n                    builder.addQueryParameter(\"path\", path)\n                }\n            }\n        }\n    }\n    if (type.isNotBlank() && type != \"none\") {\n        builder.addQueryParameter(\"encryption\", encryption)\n    }\n    if (plugin.isNotBlank()) {\n        builder.addQueryParameter(\"plugin\", plugin)\n    }\n\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n    return builder.toLink(\"trojan-go\")\n}\n\nfun TrojanGoBean.buildTrojanGoConfig(port: Int, mux: Boolean): String {\n    return JSONObject().also { conf ->\n        conf[\"run_type\"] = \"client\"\n        conf[\"local_addr\"] = LOCALHOST\n        conf[\"local_port\"] = port\n        conf[\"remote_addr\"] = finalAddress\n        conf[\"remote_port\"] = finalPort\n        conf[\"password\"] = JSONArray().apply {\n            add(password)\n        }\n        conf[\"log_level\"] = if (DataStore.enableLog) 0 else 2\n        if (mux) conf[\"mux\"] = JSONObject().also {\n            it[\"enabled\"] = true\n            it[\"concurrency\"] = DataStore.muxConcurrency\n        }\n        conf[\"tcp\"] = JSONObject().also {\n            it[\"prefer_ipv4\"] = DataStore.ipv6Mode <= IPv6Mode.ENABLE\n        }\n\n        when (type) {\n            \"original\" -> {\n            }\n            \"ws\" -> conf[\"websocket\"] = JSONObject().also {\n                it[\"enabled\"] = true\n                it[\"host\"] = host\n                it[\"path\"] = path\n            }\n        }\n\n        if (sni.isBlank() && finalAddress == LOCALHOST && !serverAddress.isIpAddress()) {\n            sni = serverAddress\n        }\n\n        conf[\"ssl\"] = JSONObject().also {\n            if (sni.isNotBlank()) it[\"sni\"] = sni\n        }\n\n        when {\n            encryption == \"none\" -> {\n            }\n            encryption.startsWith(\"ss;\") -> conf[\"shadowsocks\"] = JSONObject().also {\n                it[\"enabled\"] = true\n                it[\"method\"] = encryption.substringAfter(\";\").substringBefore(\":\")\n                it[\"password\"] = encryption.substringAfter(\":\")\n            }\n        }\n\n        if (plugin.isNotBlank()) {\n            val pluginConfiguration = PluginConfiguration(plugin ?: \"\")\n            PluginManager.init(pluginConfiguration)?.let { (path, opts, isV2) ->\n                conf[\"transport_plugin\"] = JSONObject().also {\n                    it[\"enabled\"] = true\n                    it[\"type\"] = \"shadowsocks\"\n                    it[\"command\"] = path\n                    it[\"option\"] = opts.toString()\n                }\n            }\n        }\n    }.toStringPretty()\n}\n\nfun buildCustomTrojanConfig(config: String, port: Int): String {\n    val conf = JSONObject(config)\n    conf[\"local_port\"] = port\n    return conf.toStringPretty()\n}\n\nfun JSONObject.parseTrojanGo(): TrojanGoBean {\n    return TrojanGoBean().applyDefaultValues().apply {\n        serverAddress = getStr(\"remote_addr\", serverAddress)\n        serverPort = getInt(\"remote_port\", serverPort)\n        when (val pass = get(\"password\")) {\n            is String -> {\n                password = pass\n            }\n            is List<*> -> {\n                password = pass[0] as String\n            }\n        }\n        getJSONObject(\"ssl\")?.apply {\n            sni = getStr(\"sni\", sni)\n        }\n        getJSONObject(\"websocket\")?.apply {\n            if (getBool(\"enabled\", false)) {\n                type = \"ws\"\n                host = getStr(\"host\", host)\n                path = getStr(\"path\", path)\n            }\n        }\n        getJSONObject(\"shadowsocks\")?.apply {\n            if (getBool(\"enabled\", false)) {\n                encryption = \"ss;${getStr(\"method\", \"\")}:${getStr(\"password\", \"\")}\"\n            }\n        }\n        getJSONObject(\"transport_plugin\")?.apply {\n            if (getBool(\"enabled\", false)) {\n                when (type) {\n                    \"shadowsocks\" -> {\n                        val pl = PluginConfiguration()\n                        pl.selected = getStr(\"command\")\n                        getJSONArray(\"arg\")?.also {\n                            pl.pluginsOptions[pl.selected] = PluginOptions().also { opts ->\n                                var key = \"\"\n                                it.forEachIndexed { index, param ->\n                                    if (index % 2 != 0) {\n                                        key = param.toString()\n                                    } else {\n                                        opts[key] = param.toString()\n                                    }\n                                }\n                            }\n                        }\n                        getStr(\"option\")?.also {\n                            pl.pluginsOptions[pl.selected] = PluginOptions(it)\n                        }\n                        pl.fixInvalidParams()\n                        plugin = pl.toString()\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.v2ray;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport cn.hutool.core.lang.UUID;\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.ktx.UUIDsKt;\n\n/**\n * https://github.com/XTLS/Xray-core/issues/91\n */\npublic abstract class StandardV2RayBean extends AbstractBean {\n\n    /**\n     * UUID。对应配置文件该项出站中 settings.vnext[0].users[0].id 的值。\n     * <p>\n     * 不可省略，不能为空字符串。\n     */\n    public String uuid;\n\n    /**\n     * 当协议为 VMess 时，对应配置文件出站中 settings.security，可选值为 auto / aes-128-gcm / chacha20-poly1305 / none。\n     * <p>\n     * 省略时默认为 auto，但不可以为空字符串。除非指定为 none，否则建议省略。\n     * <p>\n     * 当协议为 VLESS 时，对应配置文件出站中 settings.encryption，当前可选值只有 none。\n     * <p>\n     * 省略时默认为 none，但不可以为空字符串。\n     * <p>\n     * 特殊说明：之所以不使用 security 而使用 encryption，是因为后面还有一个底层传输安全类型 security 与这个冲突。\n     * 由 @huyz 提议，将此字段重命名为 encryption，这样不仅能避免命名冲突，还与 VLESS 保持了一致。\n     */\n    public String encryption;\n\n    /**\n     * 协议的传输方式。对应配置文件出站中 settings.vnext[0].streamSettings.network 的值。\n     * <p>\n     * 当前的取值必须为 tcp、kcp、ws、http、quic 其中之一，分别对应 TCP、mKCP、WebSocket、HTTP/2、QUIC 传输方式。\n     */\n    public String type;\n\n    /**\n     * 客户端进行 HTTP/2 通信时所发送的 Host 头部。\n     * <p>\n     * 省略时复用 remote-host，但不可以为空字符串。\n     * <p>\n     * 若有多个域名，可使用英文逗号隔开，但中间及前后不可有空格。\n     * <p>\n     * 必须使用 encodeURIComponent 转义。\n     * -----------------------------------\n     * WebSocket 请求时 Host 头的内容。不推荐省略，不推荐设为空字符串。\n     * <p>\n     * 必须使用 encodeURIComponent 转义。\n     */\n    public String host;\n\n    /**\n     * HTTP/2 的路径。省略时默认为 /，但不可以为空字符串。不推荐省略。\n     * <p>\n     * 必须使用 encodeURIComponent 转义。\n     * -----------------------------------\n     * WebSocket 的路径。省略时默认为 /，但不可以为空字符串。不推荐省略。\n     * <p>\n     * 必须使用 encodeURIComponent 转义。\n     */\n    public String path;\n\n    /**\n     * mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。\n     * <p>\n     * 省略时默认值为 none，即不使用伪装头部，但不可以为空字符串。\n     * -----------------------------------\n     * QUIC 的伪装头部类型。其他同 mKCP headerType 字段定义。\n     */\n    public String headerType;\n\n    /**\n     * mKCP 种子。省略时不使用种子，但不可以为空字符串。建议 mKCP 用户使用 seed。\n     * <p>\n     * 必须使用 encodeURIComponent 转义。\n     */\n    public String mKcpSeed;\n\n    /**\n     * QUIC 的加密方式。当前可选值有 none / aes-128-gcm / chacha20-poly1305。\n     * <p>\n     * 省略时默认值为 none。\n     */\n    public String quicSecurity;\n\n    /**\n     * 当 QUIC 的加密方式不为 none 时的加密密钥。\n     * <p>\n     * 当 QUIC 的加密方式为 none 时，此项不得出现；否则，此项必须出现，且不可为空字符串。\n     * <p>\n     * 若出现此项，则必须使用 encodeURIComponent 转义。\n     */\n    public String quicKey;\n\n    /**\n     * 底层传输安全 security\n     * <p>\n     * 设定底层传输所使用的 TLS 类型。当前可选值有 none，tls 和 xtls。\n     * <p>\n     * 省略时默认为 none，但不可以为空字符串。\n     */\n    public String security;\n\n    /**\n     * TLS SNI，对应配置文件中的 serverName 项目。\n     * <p>\n     * 省略时复用 remote-host，但不可以为空字符串。\n     */\n    public String sni;\n\n    /**\n     * TLS ALPN，对应配置文件中的 alpn 项目。\n     * <p>\n     * 多个 ALPN 之间用英文逗号隔开，中间无空格。\n     * <p>\n     * 省略时由内核决定具体行为，但不可以为空字符串。\n     * <p>\n     * 必须使用 encodeURIComponent 转义。\n     */\n    public String alpn;\n\n    /**\n     * XTLS 的流控方式。可选值为 xtls-rprx-direct、xtls-rprx-splice 等。\n     * <p>\n     * 若使用 XTLS，此项不可省略，否则无此项。此项不可为空字符串。\n     */\n    public String flow;\n\n    // --------------------------------------- //\n\n    public String grpcServiceName;\n    public Boolean grpcMultiMode;\n    public String earlyDataHeaderName;\n\n    public String certificates;\n\n    // --------------------------------------- //\n\n    public Boolean wsUseBrowserForwarder;\n    public Boolean allowInsecure;\n\n    // --------------------------------------- //\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (StrUtil.isBlank(uuid)) uuid = \"\";\n\n        if (StrUtil.isBlank(type)) type = \"tcp\";\n        else if (\"h2\".equals(type)) type = \"http\";\n\n        if (StrUtil.isBlank(host)) host = \"\";\n        if (StrUtil.isBlank(path)) path = \"\";\n        if (StrUtil.isBlank(headerType)) headerType = \"\";\n        if (StrUtil.isBlank(mKcpSeed)) mKcpSeed = \"\";\n        if (StrUtil.isBlank(quicSecurity)) quicSecurity = \"\";\n        if (StrUtil.isBlank(quicKey)) quicKey = \"\";\n\n        if (StrUtil.isBlank(security)) security = \"\";\n        if (StrUtil.isBlank(sni)) sni = \"\";\n        if (StrUtil.isBlank(alpn)) alpn = \"\";\n\n        if (StrUtil.isBlank(grpcServiceName)) grpcServiceName = \"\";\n        if (grpcMultiMode == null) grpcMultiMode = false;\n\n        if (wsUseBrowserForwarder == null) wsUseBrowserForwarder = false;\n        if (certificates == null) certificates = \"\";\n        if (StrUtil.isBlank(flow)) flow = \"\";\n        if (allowInsecure == null) allowInsecure = false;\n\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(5);\n        super.serialize(output);\n\n        output.writeString(uuid);\n        output.writeString(encryption);\n        output.writeString(type);\n\n        switch (type) {\n            case \"tcp\": {\n                output.writeString(headerType);\n                output.writeString(host);\n                output.writeString(path);\n                break;\n            }\n            case \"kcp\": {\n                output.writeString(headerType);\n                output.writeString(mKcpSeed);\n                break;\n            }\n            case \"ws\": {\n                output.writeString(host);\n                output.writeString(path);\n                output.writeBoolean(wsUseBrowserForwarder);\n                break;\n            }\n            case \"http\": {\n                output.writeString(host);\n                output.writeString(path);\n                break;\n            }\n            case \"quic\": {\n                output.writeString(headerType);\n                output.writeString(quicSecurity);\n                output.writeString(quicKey);\n            }\n            case \"grpc\": {\n                output.writeString(grpcServiceName);\n                output.writeBoolean(grpcMultiMode);\n            }\n        }\n\n        output.writeString(security);\n\n        switch (security) {\n            case \"tls\": {\n                output.writeString(sni);\n                output.writeString(alpn);\n                output.writeString(certificates);\n                output.writeBoolean(allowInsecure);\n                break;\n            }\n            case \"xtls\": {\n                output.writeString(sni);\n                output.writeString(alpn);\n                output.writeString(flow);\n                output.writeString(certificates);\n                output.writeBoolean(allowInsecure);\n                break;\n            }\n        }\n\n        if (this instanceof VMessBean) {\n            output.writeInt(((VMessBean) this).alterId);\n            output.writeBoolean(((VMessBean) this).experimentalAuthenticatedLength);\n            output.writeBoolean(((VMessBean) this).experimentalNoTerminationSignal);\n        }\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        uuid = input.readString();\n        encryption = input.readString();\n        type = input.readString();\n\n        switch (type) {\n            case \"tcp\": {\n                headerType = input.readString();\n                host = input.readString();\n                path = input.readString();\n                break;\n            }\n            case \"kcp\": {\n                headerType = input.readString();\n                mKcpSeed = input.readString();\n                break;\n            }\n            case \"ws\": {\n                host = input.readString();\n                path = input.readString();\n                wsUseBrowserForwarder = input.readBoolean();\n                break;\n            }\n            case \"http\": {\n                host = input.readString();\n                path = input.readString();\n                break;\n            }\n            case \"quic\": {\n                headerType = input.readString();\n                quicSecurity = input.readString();\n                quicKey = input.readString();\n            }\n            case \"grpc\": {\n                grpcServiceName = input.readString();\n                grpcMultiMode = input.readBoolean();\n            }\n        }\n\n        security = input.readString();\n        switch (security) {\n            case \"tls\": {\n                sni = input.readString();\n                alpn = input.readString();\n                if (version >= 1) certificates = input.readString();\n                if (version >= 3) allowInsecure = input.readBoolean();\n                break;\n            }\n            case \"xtls\": {\n                sni = input.readString();\n                alpn = input.readString();\n                flow = input.readString();\n                if (version >= 4) certificates = input.readString();\n                if (version >= 3) allowInsecure = input.readBoolean();\n            }\n        }\n        if (this instanceof VMessBean && version != 4) {\n            ((VMessBean) this).alterId = input.readInt();\n        }\n        if (this instanceof VMessBean && version >= 4) {\n            ((VMessBean) this).experimentalAuthenticatedLength = input.readBoolean();\n            ((VMessBean) this).experimentalNoTerminationSignal = input.readBoolean();\n        }\n    }\n\n    @Override\n    public void applyFeatureSettings(AbstractBean other) {\n        if (!(other instanceof StandardV2RayBean)) return;\n        StandardV2RayBean bean = ((StandardV2RayBean) other);\n        bean.wsUseBrowserForwarder = wsUseBrowserForwarder;\n        bean.allowInsecure = allowInsecure;\n    }\n\n    public String uuidOrGenerate() {\n        try {\n            return UUID.fromString(uuid).toString(false);\n        } catch (Exception ignored) {\n            return UUIDsKt.uuid5(uuid);\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.v2ray;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.gson.annotations.SerializedName;\nimport com.google.gson.stream.JsonToken;\n\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\n\nimport io.nekohasekai.sagernet.fmt.gson.JsonLazyInterface;\nimport io.nekohasekai.sagernet.fmt.gson.JsonOr;\n\n@SuppressWarnings({\"SpellCheckingInspection\", \"unused\", \"RedundantSuppression\"})\npublic class V2RayConfig {\n\n    public LogObject log;\n\n    public static class LogObject {\n\n        public String access;\n        public String error;\n        public String loglevel;\n\n    }\n\n    public ApiObject api;\n\n    public static class ApiObject {\n\n        public String tag;\n        public List<String> services;\n\n    }\n\n    public DnsObject dns;\n\n    public static class DnsObject {\n\n        public Map<String, String> hosts;\n\n        public List<StringOrServerObject> servers;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public String clientIp;\n            public Boolean skipFallback;\n            public List<String> domains;\n            public List<String> expectIPs;\n            public Boolean concurrent;\n\n        }\n\n        public static class StringOrServerObject extends JsonOr<String, ServerObject> {\n            public StringOrServerObject() {\n                super(JsonToken.STRING, JsonToken.BEGIN_OBJECT);\n            }\n        }\n\n        public String clientIp;\n        public Boolean disableCache;\n        public String tag;\n        public List<String> domains;\n        public List<String> expectIPs;\n        public String queryStrategy;\n\n        public Boolean disableFallback;\n        public Boolean disableFallbackIfMatch;\n\n    }\n\n    public RoutingObject routing;\n\n    public static class RoutingObject {\n\n        public String domainStrategy;\n        public String domainMatcher;\n        public List<RuleObject> rules;\n\n        public static class RuleObject {\n\n            public String type;\n            public List<String> domain;\n            public List<String> ip;\n            public String port;\n            public String sourcePort;\n            public String network;\n            public List<String> source;\n            public List<String> user;\n            public List<String> inboundTag;\n            public List<String> protocol;\n            public String attrs;\n            public String outboundTag;\n            public String balancerTag;\n\n            // SagerNet private\n\n            public List<Integer> uidList;\n            public List<String> appStatus;\n\n        }\n\n        public List<BalancerObject> balancers;\n\n        public static class BalancerObject {\n\n            public String tag;\n            public List<String> selector;\n            public StrategyObject strategy;\n\n            public static class StrategyObject {\n\n                public String type;\n\n            }\n\n        }\n\n    }\n\n    public PolicyObject policy;\n\n    public static class PolicyObject {\n\n        public Map<String, LevelPolicyObject> levels;\n\n        public static class LevelPolicyObject {\n\n            public Integer handshake;\n            public Integer connIdle;\n            public Integer uplinkOnly;\n            public Integer downlinkOnly;\n            public Boolean statsUserUplink;\n            public Boolean statsUserDownlink;\n            public Integer bufferSize;\n\n        }\n\n        public SystemPolicyObject system;\n\n        public static class SystemPolicyObject {\n\n            public Boolean statsInboundUplink;\n            public Boolean statsInboundDownlink;\n            public Boolean statsOutboundUplink;\n            public Boolean statsOutboundDownlink;\n\n        }\n\n    }\n\n    public List<InboundObject> inbounds;\n\n    public static class InboundObject {\n\n        public String listen;\n        public Integer port;\n        public String protocol;\n        public LazyInboundConfigurationObject settings;\n        public StreamSettingsObject streamSettings;\n        public String tag;\n        public SniffingObject sniffing;\n        public AllocateObject allocate;\n\n        public void init() {\n            if (settings != null) {\n                settings.init(this);\n            }\n        }\n\n        public static class SniffingObject {\n\n            public Boolean enabled;\n            public List<String> destOverride;\n            public Boolean metadataOnly;\n            public Boolean routeOnly;\n\n        }\n\n        public static class AllocateObject {\n\n            public String strategy;\n            public Integer refresh;\n            public Integer concurrency;\n\n        }\n\n    }\n\n    public static class LazyInboundConfigurationObject extends JsonLazyInterface<InboundConfigurationObject> {\n\n        public LazyInboundConfigurationObject() {\n        }\n\n        public LazyInboundConfigurationObject(InboundObject ctx, InboundConfigurationObject value) {\n            super(value);\n            init(ctx);\n        }\n\n        public InboundObject ctx;\n\n        public void init(InboundObject ctx) {\n            this.ctx = ctx;\n        }\n\n        @Nullable\n        @Override\n        protected Class<? extends InboundConfigurationObject> getType() {\n            switch (ctx.protocol.toLowerCase(Locale.ROOT)) {\n                case \"dokodemo-door\":\n                    return DokodemoDoorInboundConfigurationObject.class;\n                case \"http\":\n                    return HTTPInboundConfigurationObject.class;\n                case \"socks\":\n                    return SocksInboundConfigurationObject.class;\n                case \"vmess\":\n                    return VMessInboundConfigurationObject.class;\n                case \"vless\":\n                    return VLESSInboundConfigurationObject.class;\n                case \"shadowsocks\":\n                    return ShadowsocksInboundConfigurationObject.class;\n                case \"trojan\":\n                    return TrojanInboundConfigurationObject.class;\n\n            }\n            return null;\n        }\n\n    }\n\n    public interface InboundConfigurationObject {\n    }\n\n    public static class DokodemoDoorInboundConfigurationObject implements InboundConfigurationObject {\n\n        public String address;\n        public Integer port;\n        public String network;\n        public Integer timeout;\n        public Boolean followRedirect;\n        public Integer userLevel;\n\n    }\n\n    public static class HTTPInboundConfigurationObject implements InboundConfigurationObject {\n\n        public Integer timeout;\n        public List<AccountObject> accounts;\n        public Boolean allowTransparent;\n        public Integer userLevel;\n\n        public static class AccountObject {\n\n            public String user;\n            public String pass;\n\n        }\n\n    }\n\n    public static class SocksInboundConfigurationObject implements InboundConfigurationObject {\n\n\n        public String auth;\n        public List<AccountObject> accounts;\n        public Boolean udp;\n        public String ip;\n        public Integer userLevel;\n\n        public static class AccountObject {\n\n            public String user;\n            public String pass;\n\n        }\n\n    }\n\n    public static class VMessInboundConfigurationObject implements InboundConfigurationObject {\n\n        public List<ClientObject> clients;\n        @SerializedName(\"default\")\n        public DefaultObject defaultObject;\n        public DetourObject detour;\n        public Boolean disableInsecureEncryption;\n\n\n        public static class ClientObject {\n\n            public String id;\n            public Integer level;\n            public Integer alterId;\n            public String email;\n\n        }\n\n        public static class DefaultObject {\n\n            public Integer level;\n            public Integer alterId;\n\n        }\n\n        public static class DetourObject {\n\n            public String to;\n\n        }\n\n    }\n\n    public static class VLESSInboundConfigurationObject implements InboundConfigurationObject {\n\n        public List<ClientObject> clients;\n        public String decryption;\n        public List<FallbackObject> fallbacks;\n\n        public static class ClientObject {\n\n            public String id;\n            public Integer level;\n            public String email;\n\n        }\n\n        public static class FallbackObject {\n\n            public String alpn;\n            public String path;\n            public Integer dest;\n            public Integer xver;\n\n        }\n\n    }\n\n    public static class ShadowsocksInboundConfigurationObject implements InboundConfigurationObject {\n\n        public String email;\n        public String method;\n        public String password;\n        public Integer level;\n        public String network;\n\n    }\n\n    public static class TrojanInboundConfigurationObject implements InboundConfigurationObject {\n\n        public List<ClientObject> clients;\n        public List<FallbackObject> fallbacks;\n\n        public static class ClientObject {\n\n            public String password;\n            public String email;\n            public Integer level;\n\n        }\n\n        public static class FallbackObject {\n\n            public String alpn;\n            public String path;\n            public Integer dest;\n            public Integer xver;\n\n        }\n\n    }\n\n    public List<OutboundObject> outbounds;\n\n    public static class OutboundObject {\n\n        public String sendThrough;\n        public String protocol;\n        public LazyOutboundConfigurationObject settings;\n        public String tag;\n        public StreamSettingsObject streamSettings;\n        public ProxySettingsObject proxySettings;\n        public MuxObject mux;\n        public String domainStrategy;\n        public Long fallbackDelayMs;\n\n        public void init() {\n            if (settings != null) {\n                settings.init(this);\n            }\n        }\n\n        public static class ProxySettingsObject {\n\n            public String tag;\n            public Boolean transportLayer;\n\n        }\n\n        public static class MuxObject {\n\n            public Boolean enabled;\n            public Integer concurrency;\n\n        }\n\n    }\n\n    public static class LazyOutboundConfigurationObject extends JsonLazyInterface<OutboundConfigurationObject> {\n\n        public LazyOutboundConfigurationObject() {\n        }\n\n        public LazyOutboundConfigurationObject(OutboundObject ctx, OutboundConfigurationObject value) {\n            super(value);\n            init(ctx);\n        }\n\n        private OutboundObject ctx;\n\n        public void init(OutboundObject ctx) {\n            this.ctx = ctx;\n        }\n\n        @Nullable\n        @Override\n        protected Class<? extends OutboundConfigurationObject> getType() {\n            switch (ctx.protocol.toLowerCase(Locale.ROOT)) {\n                case \"blackhole\":\n                    return BlackholeOutboundConfigurationObject.class;\n                case \"dns\":\n                    return DNSOutboundConfigurationObject.class;\n                case \"freedom\":\n                    return FreedomOutboundConfigurationObject.class;\n                case \"http\":\n                    return HTTPOutboundConfigurationObject.class;\n                case \"socks\":\n                    return SocksOutboundConfigurationObject.class;\n                case \"vmess\":\n                    return VMessOutboundConfigurationObject.class;\n                case \"vless\":\n                    return VLESSOutboundConfigurationObject.class;\n                case \"shadowsocks\":\n                    return ShadowsocksOutboundConfigurationObject.class;\n                case \"trojan\":\n                    return TrojanOutboundConfigurationObject.class;\n                case \"loopback\":\n                    return LoopbackOutboundConfigurationObject.class;\n            }\n            return null;\n        }\n    }\n\n    public interface OutboundConfigurationObject {\n    }\n\n    public static class BlackholeOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public ResponseObject response;\n        public Boolean keepConnection;\n        public Integer userLevel;\n\n        public static class ResponseObject {\n            public String type;\n        }\n\n    }\n\n    public static class DNSOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public String network;\n        public String address;\n        public Integer port;\n\n        // SagerNet private\n        public Integer userLevel;\n\n    }\n\n    public static class FreedomOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public String domainStrategy;\n        public String redirect;\n        public Integer userLevel;\n\n\n    }\n\n    public static class HTTPOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public List<ServerObject> servers;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public List<HTTPInboundConfigurationObject.AccountObject> users;\n\n        }\n\n    }\n\n    public static class SocksOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public List<ServerObject> servers;\n        public String version;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public List<UserObject> users;\n\n            public static class UserObject {\n\n                public String user;\n                public String pass;\n                public Integer level;\n\n            }\n\n        }\n\n    }\n\n    public static class VMessOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public List<ServerObject> vnext;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public List<UserObject> users;\n\n            public static class UserObject {\n\n                public String id;\n                public Integer alterId;\n                public String security;\n                public Integer level;\n                public String experimental;\n\n            }\n\n        }\n\n    }\n\n    public static class ShadowsocksOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public List<ServerObject> servers;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public String method;\n            public String password;\n            public Integer level;\n            public String email;\n\n        }\n\n    }\n\n    public static class VLESSOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public List<ServerObject> vnext;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public List<UserObject> users;\n\n            public static class UserObject {\n\n                public String id;\n                public String encryption;\n                public Integer level;\n                public String flow;\n\n            }\n\n        }\n\n    }\n\n    public static class TrojanOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public List<ServerObject> servers;\n\n        public static class ServerObject {\n\n            public String address;\n            public Integer port;\n            public String password;\n            public String email;\n            public Integer level;\n            public String flow;\n\n        }\n\n    }\n\n    public static class LoopbackOutboundConfigurationObject implements OutboundConfigurationObject {\n\n        public String inboundTag;\n\n    }\n\n    public TransportObject transport;\n\n    public static class TransportObject {\n\n        public TLSObject tlsSettings;\n        public TcpObject tcpSettings;\n        public KcpObject kcpSettings;\n        public WebSocketObject wsSettings;\n        public HttpObject httpSettings;\n        public QuicObject quicSettings;\n        public DomainSocketObject dsSettings;\n        public GrpcObject grpcSettings;\n\n    }\n\n    public static class StreamSettingsObject {\n\n        public String network;\n        public String security;\n        public TLSObject tlsSettings;\n        public TLSObject xtlsSettings;\n        public TcpObject tcpSettings;\n        public KcpObject kcpSettings;\n        public WebSocketObject wsSettings;\n        public HttpObject httpSettings;\n        public QuicObject quicSettings;\n        public DomainSocketObject dsSettings;\n        public GrpcObject grpcSettings;\n        public SockoptObject sockopt;\n\n        public static class SockoptObject {\n\n            public Integer mark;\n            public Boolean tcpFastOpen;\n            public String tproxy;\n\n        }\n\n    }\n\n    public static class TLSObject {\n\n        public String serverName;\n        public Boolean allowInsecure;\n        public List<String> alpn;\n        public List<CertificateObject> certificates;\n        public Boolean disableSystemRoot;\n        public String fingerprint;\n\n        public static class CertificateObject {\n\n            public String usage;\n            public String certificateFile;\n            public String keyFile;\n            public List<String> certificate;\n            public List<String> key;\n\n        }\n\n    }\n\n    public static class TcpObject {\n\n        public Boolean acceptProxyProtocol;\n        public HeaderObject header;\n\n        public static class HeaderObject {\n\n            public String type;\n\n            public HTTPRequestObject request;\n            public HTTPResponseObject response;\n\n            public static class HTTPRequestObject {\n\n                public String version;\n                public String method;\n                public List<String> path;\n                public Map<String, StringOrListObject> headers;\n\n            }\n\n            public static class HTTPResponseObject {\n\n                public String version;\n                public String status;\n                public String reason;\n                public Map<String, StringOrListObject> headers;\n\n            }\n\n            public static class StringOrListObject extends JsonOr<String, List<String>> {\n                public StringOrListObject() {\n                    super(JsonToken.STRING, JsonToken.BEGIN_ARRAY);\n                }\n            }\n\n        }\n\n    }\n\n\n    public static class KcpObject {\n\n        public Integer mtu;\n        public Integer tti;\n        public Integer uplinkCapacity;\n        public Integer downlinkCapacity;\n        public Boolean congestion;\n        public Integer readBufferSize;\n        public Integer writeBufferSize;\n        public HeaderObject header;\n        public String seed;\n\n        public static class HeaderObject {\n\n            public String type;\n\n        }\n\n    }\n\n    public static class WebSocketObject {\n\n        public Boolean acceptProxyProtocol;\n        public String path;\n        public Map<String, String> headers;\n\n    }\n\n    public static class HttpObject {\n\n        public List<String> host;\n        public String path;\n\n    }\n\n    public static class QuicObject {\n\n        public String security;\n        public String key;\n        public HeaderObject header;\n\n        public static class HeaderObject {\n\n            public String type;\n\n        }\n\n    }\n\n    public static class DomainSocketObject {\n\n        public String path;\n        @SerializedName(\"abstract\")\n        public Boolean isAbstract;\n        public Boolean padding;\n\n    }\n\n    public static class GrpcObject {\n\n        public String serviceName;\n        public Boolean multiMode;\n\n    }\n\n    public Map<String, Object> stats;\n\n    public FakeDnsObject fakedns;\n\n    public static class FakeDnsObject {\n\n        public String ipPool;\n        public Integer poolSize;\n\n    }\n\n    public ReverseObject reverse;\n\n    public static class ReverseObject {\n        public List<BridgeObject> bridges;\n        public List<PortalObject> portals;\n\n        public static class BridgeObject {\n            public String tag;\n            public String domain;\n        }\n\n        public static class PortalObject {\n            public String tag;\n            public String domain;\n        }\n    }\n\n    public ObservatoryObject observatory;\n\n    public static class ObservatoryObject {\n        public Set<String> subjectSelector;\n        public String probeUrl;\n        public String probeInterval;\n        public Boolean enableConcurrency;\n    }\n\n    public void init() {\n        if (inbounds != null) {\n            for (InboundObject inbound : inbounds) inbound.init();\n        }\n        if (outbounds != null) {\n            for (OutboundObject outbound : outbounds) outbound.init();\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.v2ray\n\nimport cn.hutool.core.codec.Base64\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.ktx.*\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nfun parseV2Ray(link: String): StandardV2RayBean {\n    if (!link.contains(\"@\")) {\n        return parseV2RayN(link)\n    }\n\n    val bean = if (!link.startsWith(\"vless://\")) {\n        VMessBean()\n    } else {\n        VLESSBean()\n    }\n    val url = link.replace(\"vmess://\", \"https://\").replace(\"vless://\", \"https://\").toHttpUrl()\n\n    bean.serverAddress = url.host\n    bean.serverPort = url.port\n    bean.name = url.fragment\n\n    if (url.password.isNotBlank()) { // https://github.com/v2fly/v2fly-github-io/issues/26\n        (bean as VMessBean?) ?: error(\"Invalid vless url: $link\")\n\n        var protocol = url.username\n        bean.type = protocol\n        bean.alterId = url.password.substringAfterLast('-').toInt()\n        bean.uuid = url.password.substringBeforeLast('-')\n\n        if (protocol.endsWith(\"+tls\")) {\n            bean.security = \"tls\"\n            protocol = protocol.substring(0, protocol.length - 4)\n\n            url.queryParameter(\"tlsServerName\")?.let {\n                if (it.isNotBlank()) {\n                    bean.sni = it\n                }\n            }\n        }\n\n        when (protocol) {\n            \"tcp\" -> {\n                url.queryParameter(\"type\")?.let { type ->\n                    if (type == \"http\") {\n                        bean.headerType = \"http\"\n                        url.queryParameter(\"host\")?.let {\n                            bean.host = it\n                        }\n                    }\n                }\n            }\n            \"http\" -> {\n                url.queryParameter(\"path\")?.let {\n                    bean.path = it\n                }\n                url.queryParameter(\"host\")?.let {\n                    bean.host = it.split(\"|\").joinToString(\",\")\n                }\n            }\n            \"ws\" -> {\n                url.queryParameter(\"path\")?.let {\n                    bean.path = it\n                }\n                url.queryParameter(\"host\")?.let {\n                    bean.host = it\n                }\n            }\n            \"kcp\" -> {\n                url.queryParameter(\"type\")?.let {\n                    bean.headerType = it\n                }\n                url.queryParameter(\"seed\")?.let {\n                    bean.mKcpSeed = it\n                }\n            }\n            \"quic\" -> {\n                url.queryParameter(\"security\")?.let {\n                    bean.quicSecurity = it\n                }\n                url.queryParameter(\"key\")?.let {\n                    bean.quicKey = it\n                }\n                url.queryParameter(\"type\")?.let {\n                    bean.headerType = it\n                }\n            }\n        }\n    } else { // https://github.com/XTLS/Xray-core/issues/91\n\n        bean.uuid = url.username\n        if (url.pathSegments.size > 1 || url.pathSegments[0].isNotBlank()) {\n            bean.path = url.pathSegments.joinToString(\"/\")\n        }\n\n        val protocol = url.queryParameter(\"type\") ?: \"tcp\"\n        bean.type = protocol\n\n        when (url.queryParameter(\"security\")) {\n            \"tls\" -> {\n                bean.security = \"tls\"\n                url.queryParameter(\"sni\")?.let {\n                    bean.sni = it\n                }\n                url.queryParameter(\"alpn\")?.let {\n                    bean.alpn = it\n                }\n                url.queryParameter(\"cert\")?.let {\n                    bean.certificates = it\n                }\n            }\n            \"xtls\" -> {\n                bean.security = \"xtls\"\n                url.queryParameter(\"sni\")?.let {\n                    bean.sni = it\n                }\n                url.queryParameter(\"alpn\")?.let {\n                    bean.alpn = it\n                }\n                url.queryParameter(\"flow\")?.let {\n                    bean.flow = it\n                }\n                url.queryParameter(\"cert\")?.let {\n                    bean.certificates = it\n                }\n            }\n        }\n        when (protocol) {\n            \"tcp\" -> {\n                url.queryParameter(\"headerType\")?.let { headerType ->\n                    if (headerType == \"http\") {\n                        bean.headerType = headerType\n                        url.queryParameter(\"host\")?.let {\n                            bean.host = it\n                        }\n                        url.queryParameter(\"path\")?.let {\n                            bean.path = it\n                        }\n                    }\n                }\n            }\n            \"kcp\" -> {\n                url.queryParameter(\"headerType\")?.let {\n                    bean.headerType = it\n                }\n                url.queryParameter(\"seed\")?.let {\n                    bean.mKcpSeed = it\n                }\n            }\n            \"http\" -> {\n                url.queryParameter(\"host\")?.let {\n                    bean.host = it\n                }\n                url.queryParameter(\"path\")?.let {\n                    bean.path = it\n                }\n            }\n            \"ws\" -> {\n                url.queryParameter(\"host\")?.let {\n                    bean.host = it\n                }\n                url.queryParameter(\"path\")?.let {\n                    bean.path = it\n                }\n            }\n            \"quic\" -> {\n                url.queryParameter(\"headerType\")?.let {\n                    bean.headerType = it\n                }\n                url.queryParameter(\"quicSecurity\")?.let { quicSecurity ->\n                    bean.quicSecurity = quicSecurity\n                    url.queryParameter(\"key\")?.let {\n                        bean.quicKey = it\n                    }\n                }\n            }\n            \"grpc\" -> {\n                url.queryParameter(\"serviceName\")?.let {\n                    bean.grpcServiceName = it\n                }\n                url.queryParameter(\"mode\")?.takeIf { it == \"multi\" }?.also {\n                    bean.grpcMultiMode = true\n                }\n            }\n        }\n    }\n\n    Logs.d(formatObject(bean))\n\n    return bean\n}\n\nfun parseV2RayN(link: String): VMessBean {\n    val result = link.substringAfter(\"vmess://\").decodeBase64UrlSafe()\n    if (result.contains(\"= vmess\")) {\n        return parseCsvVMess(result)\n    }\n    val bean = VMessBean()\n    val json = JSONObject(result)\n\n    bean.serverAddress = json.getStr(\"add\") ?: \"\"\n    bean.serverPort = json.getInt(\"port\") ?: 1080\n    bean.encryption = json.getStr(\"scy\") ?: \"\"\n    bean.uuid = json.getStr(\"id\") ?: \"\"\n    bean.alterId = json.getInt(\"aid\") ?: 0\n    bean.type = json.getStr(\"net\") ?: \"\"\n    bean.headerType = json.getStr(\"type\") ?: \"\"\n    bean.host = json.getStr(\"host\") ?: \"\"\n    bean.path = json.getStr(\"path\") ?: \"\"\n\n    when (bean.headerType) {\n        \"quic\" -> {\n            bean.quicSecurity = bean.host\n            bean.quicKey = bean.path\n        }\n        \"kcp\" -> {\n            bean.mKcpSeed = bean.path\n        }\n        \"grpc\" -> {\n            bean.grpcServiceName = bean.path\n        }\n    }\n\n    bean.name = json.getStr(\"ps\") ?: \"\"\n    bean.sni = json.getStr(\"sni\") ?: bean.host\n    bean.security = json.getStr(\"tls\")\n\n    if (json.getInt(\"v\", 2) < 2) {\n        when (bean.type) {\n            \"ws\" -> {\n                var path = \"\"\n                var host = \"\"\n                val lstParameter = bean.host.split(\";\")\n                if (lstParameter.isNotEmpty()) {\n                    path = lstParameter[0].trim()\n                }\n                if (lstParameter.size > 1) {\n                    path = lstParameter[0].trim()\n                    host = lstParameter[1].trim()\n                }\n                bean.path = path\n                bean.host = host\n            }\n            \"h2\" -> {\n                var path = \"\"\n                var host = \"\"\n                val lstParameter = bean.host.split(\";\")\n                if (lstParameter.isNotEmpty()) {\n                    path = lstParameter[0].trim()\n                }\n                if (lstParameter.size > 1) {\n                    path = lstParameter[0].trim()\n                    host = lstParameter[1].trim()\n                }\n                bean.path = path\n                bean.host = host\n            }\n        }\n    }\n\n    return bean\n\n}\n\nprivate fun parseCsvVMess(csv: String): VMessBean {\n\n    val args = csv.split(\",\")\n\n    val bean = VMessBean()\n\n    bean.serverAddress = args[1]\n    bean.serverPort = args[2].toInt()\n    bean.encryption = args[3]\n    bean.uuid = args[4].replace(\"\\\"\", \"\")\n\n    args.subList(5, args.size).forEach {\n\n        when {\n            it == \"over-tls=true\" -> bean.security = \"tls\"\n            it.startsWith(\"tls-host=\") -> bean.host = it.substringAfter(\"=\")\n            it.startsWith(\"obfs=\") -> bean.type = it.substringAfter(\"=\")\n            it.startsWith(\"obfs-path=\") || it.contains(\"Host:\") -> {\n                runCatching {\n                    bean.path = it.substringAfter(\"obfs-path=\\\"\").substringBefore(\"\\\"obfs\")\n                }\n                runCatching {\n                    bean.host = it.substringAfter(\"Host:\").substringBefore(\"[\")\n                }\n\n            }\n\n        }\n\n    }\n\n    return bean\n\n}\n\nfun VMessBean.toV2rayN(): String {\n\n    return \"vmess://\" + JSONObject().also {\n\n        it[\"v\"] = 2\n        it[\"ps\"] = name\n        it[\"add\"] = serverAddress\n        it[\"port\"] = serverPort\n        it[\"id\"] = uuid\n        it[\"aid\"] = alterId\n        it[\"net\"] = type\n        it[\"host\"] = host\n        it[\"path\"] = path\n        it[\"type\"] = headerType\n\n        when (headerType) {\n            \"quic\" -> {\n                it[\"host\"] = quicSecurity\n                it[\"path\"] = quicKey\n            }\n            \"kcp\" -> {\n                it[\"path\"] = mKcpSeed\n            }\n            \"grpc\" -> {\n                it[\"path\"] = grpcServiceName\n            }\n        }\n\n        it[\"tls\"] = if (security == \"tls\") \"tls\" else \"\"\n        it[\"sni\"] = sni\n        it[\"scy\"] = encryption\n\n    }.toString().let { Base64.encodeUrlSafe(it) }\n\n}\n\nfun StandardV2RayBean.toUri(standard: Boolean = true): String {\n    if (this is VMessBean && alterId > 0) return toV2rayN()\n\n    val builder = linkBuilder().username(uuid).host(serverAddress).port(serverPort)\n        .addQueryParameter(\"type\", type).addQueryParameter(\"encryption\", encryption)\n\n    when (type) {\n        \"tcp\" -> {\n            if (headerType == \"http\") {\n                builder.addQueryParameter(\"headerType\", headerType)\n\n                if (host.isNotBlank()) {\n                    builder.addQueryParameter(\"host\", host)\n                }\n                if (path.isNotBlank()) {\n                    if (standard) {\n                        builder.addQueryParameter(\"path\", path)\n                    } else {\n                        builder.encodedPath(path.pathSafe())\n                    }\n                }\n            }\n        }\n        \"kcp\" -> {\n            if (headerType.isNotBlank() && headerType != \"none\") {\n                builder.addQueryParameter(\"headerType\", headerType)\n            }\n            if (mKcpSeed.isNotBlank()) {\n                builder.addQueryParameter(\"seed\", mKcpSeed)\n            }\n        }\n        \"ws\", \"http\" -> {\n            if (host.isNotBlank()) {\n                builder.addQueryParameter(\"host\", host)\n            }\n            if (path.isNotBlank()) {\n                if (standard) {\n                    builder.addQueryParameter(\"path\", path)\n                } else {\n                    builder.encodedPath(path.pathSafe())\n                }\n            }\n        }\n        \"quic\" -> {\n            if (headerType.isNotBlank() && headerType != \"none\") {\n                builder.addQueryParameter(\"headerType\", headerType)\n            }\n            if (quicSecurity.isNotBlank() && quicSecurity != \"none\") {\n                builder.addQueryParameter(\"quicSecurity\", quicSecurity)\n                builder.addQueryParameter(\"key\", quicKey)\n            }\n        }\n        \"grpc\" -> {\n            if (grpcServiceName.isNotBlank()) {\n                builder.addQueryParameter(\"serviceName\", grpcServiceName)\n            }\n            if (grpcMultiMode == true) {\n                builder.addQueryParameter(\"mode\", \"multi\")\n            }\n        }\n    }\n\n    if (security.isNotBlank() && security != \"none\") {\n        builder.addQueryParameter(\"security\", security)\n        when (security) {\n            \"tls\" -> {\n                if (sni.isNotBlank()) {\n                    builder.addQueryParameter(\"sni\", sni)\n                }\n                if (alpn.isNotBlank()) {\n                    builder.addQueryParameter(\"alpn\", alpn)\n                }\n                if (certificates.isNotBlank()) {\n                    builder.addQueryParameter(\"cert\", certificates)\n                }\n            }\n            \"xtls\" -> {\n                if (sni.isNotBlank()) {\n                    builder.addQueryParameter(\"sni\", sni)\n                }\n                if (alpn.isNotBlank()) {\n                    builder.addQueryParameter(\"alpn\", alpn)\n                }\n                if (flow.isNotBlank()) {\n                    builder.addQueryParameter(\"flow\", flow)\n                }\n                if (certificates.isNotBlank()) {\n                    builder.addQueryParameter(\"cert\", certificates)\n                }\n            }\n        }\n    }\n\n    if (name.isNotBlank()) {\n        builder.encodedFragment(name.urlSafe())\n    }\n\n    return builder.toLink(if (this is VMessBean) \"vmess\" else \"vless\")\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VLESSBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.v2ray;\n\nimport androidx.annotation.NonNull;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class VLESSBean extends StandardV2RayBean {\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        if (StrUtil.isBlank(encryption)) {\n            encryption = \"none\";\n        }\n\n    }\n\n    @NotNull\n    @Override\n    public VLESSBean clone() {\n        return KryoConverters.deserialize(new VLESSBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<VLESSBean> CREATOR = new CREATOR<VLESSBean>() {\n        @NonNull\n        @Override\n        public VLESSBean newInstance() {\n            return new VLESSBean();\n        }\n\n        @Override\n        public VLESSBean[] newArray(int size) {\n            return new VLESSBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessBean.java",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.v2ray;\n\nimport androidx.annotation.NonNull;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport cn.hutool.core.util.StrUtil;\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class VMessBean extends StandardV2RayBean {\n\n    public Integer alterId;\n\n    public Boolean experimentalAuthenticatedLength;\n    public Boolean experimentalNoTerminationSignal;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n\n        alterId = alterId != null ? alterId : 0;\n        encryption = StrUtil.isNotBlank(encryption) ? encryption : \"auto\";\n        experimentalAuthenticatedLength = experimentalAuthenticatedLength != null ? experimentalAuthenticatedLength : false;\n        experimentalNoTerminationSignal = experimentalNoTerminationSignal != null ? experimentalNoTerminationSignal : false;\n    }\n\n    @Override\n    public void applyFeatureSettings(AbstractBean other) {\n        if (!(other instanceof VMessBean)) return;\n        VMessBean bean = ((VMessBean) other);\n        bean.experimentalAuthenticatedLength = experimentalAuthenticatedLength;\n        bean.experimentalNoTerminationSignal = experimentalNoTerminationSignal;\n    }\n\n    @NotNull\n    @Override\n    public VMessBean clone() {\n        return KryoConverters.deserialize(new VMessBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<VMessBean> CREATOR = new CREATOR<VMessBean>() {\n        @NonNull\n        @Override\n        public VMessBean newInstance() {\n            return new VMessBean();\n        }\n\n        @Override\n        public VMessBean[] newArray(int size) {\n            return new VMessBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardBean.java",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.wireguard;\n\nimport androidx.annotation.NonNull;\n\nimport com.esotericsoftware.kryo.io.ByteBufferInput;\nimport com.esotericsoftware.kryo.io.ByteBufferOutput;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport io.nekohasekai.sagernet.fmt.AbstractBean;\nimport io.nekohasekai.sagernet.fmt.KryoConverters;\n\npublic class WireGuardBean extends AbstractBean {\n\n    public String localAddress;\n    public String privateKey;\n    public String peerPublicKey;\n    public String peerPreSharedKey;\n\n    @Override\n    public void initializeDefaultValues() {\n        super.initializeDefaultValues();\n        if (localAddress == null) localAddress = \"\";\n        if (privateKey == null) privateKey = \"\";\n        if (peerPublicKey == null) peerPublicKey = \"\";\n        if (peerPreSharedKey == null) peerPreSharedKey = \"\";\n    }\n\n    @Override\n    public void serialize(ByteBufferOutput output) {\n        output.writeInt(0);\n        super.serialize(output);\n        output.writeString(localAddress);\n        output.writeString(privateKey);\n        output.writeString(peerPublicKey);\n        output.writeString(peerPreSharedKey);\n    }\n\n    @Override\n    public void deserialize(ByteBufferInput input) {\n        int version = input.readInt();\n        super.deserialize(input);\n        localAddress = input.readString();\n        privateKey = input.readString();\n        peerPublicKey = input.readString();\n        peerPreSharedKey = input.readString();\n    }\n\n    @NotNull\n    @Override\n    public WireGuardBean clone() {\n        return KryoConverters.deserialize(new WireGuardBean(), KryoConverters.serialize(this));\n    }\n\n    public static final Creator<WireGuardBean> CREATOR = new CREATOR<WireGuardBean>() {\n        @NonNull\n        @Override\n        public WireGuardBean newInstance() {\n            return new WireGuardBean();\n        }\n\n        @Override\n        public WireGuardBean[] newArray(int size) {\n            return new WireGuardBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n\npackage io.nekohasekai.sagernet.fmt.wireguard\n\nimport cn.hutool.core.codec.Base64\nimport cn.hutool.core.util.HexUtil\nimport com.wireguard.crypto.Key\nimport io.nekohasekai.sagernet.ktx.wrapUri\n\nfun WireGuardBean.buildWireGuardUapiConf(): String {\n\n    var conf = \"private_key=\"\n    conf += Key.fromBase64(privateKey).toHex()\n    conf += \"\\npublic_key=\"\n    conf += Key.fromBase64(peerPublicKey).toHex()\n    if (peerPreSharedKey.isNotBlank()) {\n        conf += \"\\npreshared_key=\"\n        conf += HexUtil.encodeHexStr(Base64.decode(peerPreSharedKey))\n    }\n    conf += \"\\nendpoint=${wrapUri()}\"\n    conf += \"\\nallowed_ip=0.0.0.0/0\"\n    conf += \"\\nallowed_ip=::/0\"\n    return conf\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/group/GroupInterfaceAdapter.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.group\n\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.GroupManager\nimport io.nekohasekai.sagernet.database.ProxyGroup\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnMainDispatcher\nimport io.nekohasekai.sagernet.ui.ThemedActivity\nimport kotlinx.coroutines.delay\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\nclass GroupInterfaceAdapter(val context: ThemedActivity) : GroupManager.Interface {\n\n    override suspend fun confirm(message: String): Boolean {\n        return suspendCoroutine {\n            runOnMainDispatcher {\n                MaterialAlertDialogBuilder(context).setTitle(R.string.confirm)\n                    .setMessage(message)\n                    .setPositiveButton(R.string.yes) { _, _ -> it.resume(true) }\n                    .setNegativeButton(R.string.no) { _, _ -> it.resume(false) }\n                    .setOnCancelListener { _ -> it.resume(false) }\n                    .show()\n            }\n        }\n    }\n\n    override suspend fun onUpdateSuccess(\n        group: ProxyGroup,\n        changed: Int,\n        added: List<String>,\n        updated: Map<String, String>,\n        deleted: List<String>,\n        duplicate: List<String>,\n        byUser: Boolean\n    ) {\n        if (changed == 0 && duplicate.isEmpty()) {\n            if (byUser) context.snackbar(\n                    context.getString(\n                            R.string.group_no_difference, group.displayName()\n                    )\n            ).show()\n        } else {\n            context.snackbar(context.getString(R.string.group_updated, group.name, changed)).show()\n\n            var status = \"\"\n            if (added.isNotEmpty()) {\n                status += context.getString(\n                        R.string.group_added, added.joinToString(\"\\n\", postfix = \"\\n\\n\")\n                )\n            }\n            if (updated.isNotEmpty()) {\n                status += context.getString(R.string.group_changed,\n                        updated.map { it }.joinToString(\"\\n\", postfix = \"\\n\\n\") {\n                            if (it.key == it.value) it.key else \"${it.key} => ${it.value}\"\n                        })\n            }\n            if (deleted.isNotEmpty()) {\n                status += context.getString(\n                        R.string.group_deleted, deleted.joinToString(\"\\n\", postfix = \"\\n\\n\")\n                )\n            }\n            if (duplicate.isNotEmpty()) {\n                status += context.getString(\n                        R.string.group_duplicate, duplicate.joinToString(\"\\n\", postfix = \"\\n\\n\")\n                )\n            }\n\n            onMainDispatcher {\n                delay(1000L)\n\n                MaterialAlertDialogBuilder(context).setTitle(\n                        context.getString(\n                                R.string.group_diff, group.displayName()\n                        )\n                ).setMessage(status.trim()).setPositiveButton(android.R.string.ok, null).show()\n            }\n\n        }\n\n    }\n\n    override suspend fun onUpdateFailure(group: ProxyGroup, message: String) {\n        onMainDispatcher {\n            context.snackbar(message).show()\n        }\n    }\n\n    override suspend fun alert(message: String) {\n        return suspendCoroutine {\n            runOnMainDispatcher {\n                MaterialAlertDialogBuilder(context).setTitle(R.string.ooc_warning)\n                    .setMessage(message)\n                    .setPositiveButton(android.R.string.ok) { _, _ -> it.resume(Unit) }\n                    .setOnCancelListener { _ -> it.resume(Unit) }\n                    .show()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.group\n\nimport io.nekohasekai.sagernet.IPv6Mode\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SubscriptionType\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.GroupManager\nimport io.nekohasekai.sagernet.database.ProxyGroup\nimport io.nekohasekai.sagernet.database.SubscriptionBean\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.brook.BrookBean\nimport io.nekohasekai.sagernet.fmt.http.HttpBean\nimport io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean\nimport io.nekohasekai.sagernet.fmt.naive.NaiveBean\nimport io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean\nimport io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean\nimport io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean\nimport io.nekohasekai.sagernet.ktx.*\nimport kotlinx.coroutines.*\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\nimport okhttp3.OkHttpClient\nimport okhttp3.dnsoverhttps.DnsOverHttps\nimport java.net.Inet4Address\nimport java.net.InetAddress\nimport java.util.*\nimport java.util.concurrent.atomic.AtomicInteger\n\n@Suppress(\"EXPERIMENTAL_API_USAGE\")\nabstract class GroupUpdater {\n\n    abstract suspend fun doUpdate(\n        proxyGroup: ProxyGroup,\n        subscription: SubscriptionBean,\n        userInterface: GroupManager.Interface?,\n        httpClient: OkHttpClient,\n        byUser: Boolean\n    )\n\n    data class Progress(\n        var max: Int\n    ) {\n        var progress by AtomicInteger()\n    }\n\n    protected suspend fun forceResolve(\n        okHttpClient: OkHttpClient, profiles: List<AbstractBean>, groupId: Long?\n    ) {\n        val connected = DataStore.startedProfile > 0\n\n        var dohUrl: String? = null\n        if (connected) {\n            val remoteDns = DataStore.remoteDns\n            when {\n                remoteDns.startsWith(\"https+local://\") -> dohUrl = remoteDns.replace(\n                    \"https+local://\", \"https://\"\n                )\n                remoteDns.startsWith(\"https://\") -> dohUrl = remoteDns\n            }\n        } else {\n            val directDns = DataStore.directDns\n            when {\n                directDns.startsWith(\"https+local://\") -> dohUrl = directDns.replace(\n                    \"https+local://\", \"https://\"\n                )\n                directDns.startsWith(\"https://\") -> dohUrl = directDns\n            }\n        }\n\n        val dohHttpUrl = dohUrl?.toHttpUrlOrNull() ?: (if (connected) {\n            \"https://1.0.0.1/dns-query\"\n        } else {\n            \"https://223.5.5.5/dns-query\"\n        }).toHttpUrl()\n\n        Logs.d(\"Using doh url $dohHttpUrl\")\n\n        val ipv6Mode = DataStore.ipv6Mode\n        val dohClient = DnsOverHttps.Builder().client(okHttpClient).url(dohHttpUrl).apply {\n            if (ipv6Mode == IPv6Mode.DISABLE) includeIPv6(false)\n        }.build()\n        val lookupPool = newFixedThreadPoolContext(5, \"DNS Lookup\")\n        val lookupJobs = mutableListOf<Job>()\n        val progress = Progress(profiles.size)\n        if (groupId != null) {\n            GroupUpdater.progress[groupId] = progress\n            GroupManager.postReload(groupId)\n        }\n        val ipv6First = ipv6Mode >= IPv6Mode.PREFER\n\n        for (profile in profiles) {\n            when (profile) {\n                // SNI rewrite unsupported\n                is BrookBean -> if (profile.protocol == \"wss\") continue\n                is NaiveBean, is RelayBatonBean -> continue\n            }\n\n            if (profile.serverAddress.isIpAddress()) continue\n\n            lookupJobs.add(GlobalScope.launch(lookupPool) {\n                try {\n                    val results = dohClient.lookup(profile.serverAddress)\n                    if (results.isEmpty()) error(\"empty response\")\n                    rewriteAddress(profile, results, ipv6First)\n                } catch (e: Exception) {\n                    Logs.d(\"Lookup ${profile.serverAddress} failed: ${e.readableMessage}\")\n                }\n                if (groupId != null) {\n                    progress.progress++\n                    GroupManager.postReload(groupId)\n                }\n            })\n        }\n\n        lookupJobs.joinAll()\n        lookupPool.close()\n    }\n\n    protected fun rewriteAddress(\n        bean: AbstractBean, addresses: List<InetAddress>, ipv6First: Boolean\n    ) {\n        val address = addresses.sortedBy { (it is Inet4Address) xor ipv6First }[0].hostAddress\n\n        with(bean) {\n            when (this) {\n                is SOCKSBean -> {\n                    if (tls && sni.isBlank()) sni = bean.serverAddress\n                }\n                is HttpBean -> {\n                    if (tls && sni.isBlank()) sni = bean.serverAddress\n                }\n                is StandardV2RayBean -> {\n                    when (security) {\n                        \"tls\" -> if (sni.isBlank()) sni = bean.serverAddress\n                    }\n                }\n                is TrojanBean -> {\n                    if (sni.isBlank()) sni = bean.serverAddress\n                }\n                is TrojanGoBean -> {\n                    if (sni.isBlank()) sni = bean.serverAddress\n                }\n                is HysteriaBean -> {\n                    if (sni.isBlank()) sni = bean.serverAddress\n                }\n            }\n\n            bean.serverAddress = address\n        }\n    }\n\n    companion object {\n\n        val updating = Collections.synchronizedSet<Long>(mutableSetOf())\n        val progress = Collections.synchronizedMap<Long, Progress>(mutableMapOf())\n\n        fun startUpdate(proxyGroup: ProxyGroup, byUser: Boolean) {\n            runOnDefaultDispatcher {\n                executeUpdate(proxyGroup, byUser)\n            }\n        }\n\n        suspend fun executeUpdate(proxyGroup: ProxyGroup, byUser: Boolean): Boolean {\n            return coroutineScope {\n                if (!updating.add(proxyGroup.id)) cancel()\n                GroupManager.postReload(proxyGroup.id)\n\n                val subscription = proxyGroup.subscription!!\n                val connected = DataStore.startedProfile > 0\n\n                val httpClient = createProxyClient()\n                val userInterface = GroupManager.userInterface\n\n                if (userInterface != null) {\n                    if ((subscription.link?.startsWith(\"http://\") == true || subscription.updateWhenConnectedOnly) && !connected) {\n                        if (!userInterface.confirm(app.getString(R.string.update_subscription_warning))) {\n                            finishUpdate(proxyGroup)\n                            cancel()\n                        }\n                    }\n                }\n\n                try {\n                    when (subscription.type) {\n                        SubscriptionType.RAW -> RawUpdater\n                        SubscriptionType.OOCv1 -> OpenOnlineConfigUpdater\n                        SubscriptionType.SIP008 -> SIP008Updater\n                        else -> error(\"wtf\")\n                    }.doUpdate(proxyGroup, subscription, userInterface, httpClient, byUser)\n                    true\n                } catch (e: Throwable) {\n                    Logs.w(e)\n                    userInterface?.onUpdateFailure(proxyGroup, e.readableMessage)\n                    finishUpdate(proxyGroup)\n                    false\n                }\n            }\n        }\n\n\n        suspend fun finishUpdate(proxyGroup: ProxyGroup) {\n            updating.remove(proxyGroup.id)\n            progress.remove(proxyGroup.id)\n            GroupManager.postUpdate(proxyGroup)\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/group/OpenOnlineConfigUpdater.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.group\n\nimport android.annotation.SuppressLint\nimport cn.hutool.core.net.DefaultTrustManager\nimport cn.hutool.core.util.CharUtil\nimport cn.hutool.crypto.digest.DigestUtil\nimport cn.hutool.json.JSONObject\nimport com.github.shadowsocks.plugin.PluginConfiguration\nimport com.github.shadowsocks.plugin.PluginOptions\nimport io.nekohasekai.sagernet.ExtraType\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.*\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.USER_AGENT_ORIGIN\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport okhttp3.*\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport java.security.cert.CertificateException\nimport java.security.cert.X509Certificate\nimport javax.net.ssl.SSLSocketFactory\n\nobject OpenOnlineConfigUpdater : GroupUpdater() {\n\n    val oocConnSpec = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)\n        .tlsVersions(TlsVersion.TLS_1_3)\n        .build()\n\n    override suspend fun doUpdate(\n        proxyGroup: ProxyGroup,\n        subscription: SubscriptionBean,\n        userInterface: GroupManager.Interface?,\n        httpClient: OkHttpClient,\n        byUser: Boolean\n    ) {\n        val apiToken: JSONObject\n        var baseLink: HttpUrl\n        val certSha256: String?\n        try {\n            apiToken = JSONObject(subscription.token)\n\n            val version = apiToken.getInt(\"version\")\n            if (version != 1) {\n                if (version != null) {\n                    error(\"Unsupported OOC version $version\")\n                } else {\n                    error(\"Missing field: version\")\n                }\n            }\n            val baseUrl = apiToken.getStr(\"baseUrl\")\n            when {\n                baseUrl.isNullOrBlank() -> {\n                    error(\"Missing field: baseUrl\")\n                }\n                baseUrl.endsWith(\"/\") -> {\n                    error(\"baseUrl must not contain a trailing slash\")\n                }\n                !baseUrl.startsWith(\"https://\") -> {\n                    error(\"Protocol scheme must be https\")\n                }\n                else -> baseLink = baseUrl.toHttpUrl()\n            }\n            val secret = apiToken.getStr(\"secret\")\n            if (secret.isNullOrBlank()) error(\"Missing field: secret\")\n            baseLink = baseLink.newBuilder()\n                .addPathSegments(secret)\n                .addPathSegments(\"ooc/v1\")\n                .build()\n\n            val userId = apiToken.getStr(\"userId\")\n            if (userId.isNullOrBlank()) error(\"Missing field: userId\")\n            baseLink = baseLink.newBuilder().addPathSegment(userId).build()\n            certSha256 = apiToken.getStr(\"certSha256\")\n            if (!certSha256.isNullOrBlank()) {\n                when {\n                    certSha256.length != 64 -> {\n                        error(\"certSha256 must be a SHA-256 hexadecimal string\")\n                    }\n                    !certSha256.all { CharUtil.isLetterLower(it) || CharUtil.isNumber(it) } -> {\n                        error(\"certSha256 must be a hexadecimal string with lowercase letters\")\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            Logs.v(\"ooc token check failed, token = ${subscription.token}\", e)\n            error(app.getString(R.string.ooc_subscription_token_invalid))\n        }\n\n        val oocHttpClient = if (certSha256.isNullOrBlank()) httpClient else httpClient.newBuilder()\n            .connectionSpecs(listOf(oocConnSpec))\n            .sslSocketFactory(\n                SSLSocketFactory.getDefault() as SSLSocketFactory, PinnedTrustManager(certSha256)\n            )\n            .build()\n\n        val response = oocHttpClient.newCall(Request.Builder().url(baseLink).header(\"User-Agent\",\n            subscription.customUserAgent.takeIf { it.isNotBlank() } ?: USER_AGENT_ORIGIN).build())\n            .execute()\n            .apply {\n                if (!isSuccessful) error(\"ERROR: HTTP $code\\n\\n${body?.string() ?: \"\"}\")\n                if (body == null) error(\"ERROR: Empty response\")\n            }\n\n        Logs.d(response.toString())\n\n        val oocResponse = JSONObject(response.body!!.string())\n        subscription.username = oocResponse.getStr(\"username\")\n        subscription.bytesUsed = oocResponse.getLong(\"bytesUsed\", -1)\n        subscription.bytesRemaining = oocResponse.getLong(\"bytesRemaining\", -1)\n        subscription.expiryDate = oocResponse.getInt(\"expiryDate\", -1)\n        subscription.protocols = oocResponse.getJSONArray(\"protocols\").filterIsInstance<String>()\n        subscription.applyDefaultValues()\n\n        for (protocol in subscription.protocols) {\n            if (protocol !in supportedProtocols) {\n                userInterface?.alert(app.getString(R.string.ooc_missing_protocol, protocol))\n            }\n        }\n\n        var profiles = mutableListOf<AbstractBean>()\n\n        for (protocol in subscription.protocols) {\n            val profilesInProtocol = oocResponse.getJSONArray(protocol)\n                .filterIsInstance<JSONObject>()\n\n            if (protocol == \"shadowsocks\") for (profile in profilesInProtocol) {\n                val bean = ShadowsocksBean()\n\n                bean.name = profile.getStr(\"name\")\n                bean.serverAddress = profile.getStr(\"address\")\n                bean.serverPort = profile.getInt(\"port\")\n                bean.method = profile.getStr(\"method\")\n                bean.password = profile.getStr(\"password\")\n\n                val pluginName = profile.getStr(\"pluginName\")\n                if (!pluginName.isNullOrBlank()) {\n                    // TODO: check plugin exists\n                    // TODO: check pluginVersion\n                    // TODO: support pluginArguments\n\n                    val pl = PluginConfiguration()\n                    pl.selected = pluginName\n                    pl.pluginsOptions[pl.selected] = PluginOptions(profile.getStr(\"pluginOptions\"))\n                    pl.fixInvalidParams()\n                    bean.plugin = pl.toString()\n                }\n\n                appendExtraInfo(profile, bean)\n\n                profiles.add(bean.applyDefaultValues())\n            }\n        }\n\n        if (subscription.forceResolve) forceResolve(httpClient, profiles, proxyGroup.id)\n\n        val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)\n        val duplicate = ArrayList<String>()\n        if (subscription.deduplication) {\n            Logs.d(\"Before deduplication: ${profiles.size}\")\n            val uniqueProfiles = LinkedHashSet<AbstractBean>()\n            val uniqueNames = HashMap<AbstractBean, String>()\n            for (proxy in profiles) {\n                if (!uniqueProfiles.add(proxy)) {\n                    val index = uniqueProfiles.indexOf(proxy)\n                    if (uniqueNames.containsKey(proxy)) {\n                        val name = uniqueNames[proxy]!!.replace(\" ($index)\", \"\")\n                        if (name.isNotBlank()) {\n                            duplicate.add(\"$name ($index)\")\n                            uniqueNames[proxy] = \"\"\n                        }\n                    }\n                    duplicate.add(proxy.displayName() + \" ($index)\")\n                } else {\n                    uniqueNames[proxy] = proxy.displayName()\n                }\n            }\n            uniqueProfiles.retainAll(uniqueNames.keys)\n            profiles = uniqueProfiles.toMutableList()\n        }\n\n        Logs.d(\"New profiles: ${profiles.size}\")\n\n        val profileMap = profiles.associateBy { it.profileId }\n        val toDelete = ArrayList<ProxyEntity>()\n        val toReplace = exists.mapNotNull { entity ->\n            val profileId = entity.requireBean().profileId\n            if (profileMap.contains(profileId)) profileId to entity else let {\n                toDelete.add(entity)\n                null\n            }\n        }.toMap()\n\n        Logs.d(\"toDelete profiles: ${toDelete.size}\")\n        Logs.d(\"toReplace profiles: ${toReplace.size}\")\n\n        val toUpdate = ArrayList<ProxyEntity>()\n        val added = mutableListOf<String>()\n        val updated = mutableMapOf<String, String>()\n        val deleted = toDelete.map { it.displayName() }\n\n        var userOrder = 1L\n        var changed = toDelete.size\n        for ((profileId, bean) in profileMap.entries) {\n            val name = bean.displayName()\n            if (toReplace.contains(profileId)) {\n                val entity = toReplace[profileId]!!\n                val existsBean = entity.requireBean()\n                existsBean.applyFeatureSettings(bean)\n                when {\n                    existsBean != bean -> {\n                        changed++\n                        entity.putBean(bean)\n                        toUpdate.add(entity)\n                        updated[entity.displayName()] = name\n\n                        Logs.d(\"Updated profile: [$profileId] $name\")\n                    }\n                    entity.userOrder != userOrder -> {\n                        entity.putBean(bean)\n                        toUpdate.add(entity)\n                        entity.userOrder = userOrder\n\n                        Logs.d(\"Reordered profile: [$profileId] $name\")\n                    }\n                    else -> {\n                        Logs.d(\"Ignored profile: [$profileId] $name\")\n                    }\n                }\n            } else {\n                changed++\n                SagerDatabase.proxyDao.addProxy(ProxyEntity(\n                    groupId = proxyGroup.id, userOrder = userOrder\n                ).apply {\n                    putBean(bean)\n                })\n                added.add(name)\n                Logs.d(\"Inserted profile: $name\")\n            }\n            userOrder++\n        }\n\n        SagerDatabase.proxyDao.updateProxy(toUpdate).also {\n            Logs.d(\"Updated profiles: $it\")\n        }\n\n        SagerDatabase.proxyDao.deleteProxy(toDelete).also {\n            Logs.d(\"Deleted profiles: $it\")\n        }\n\n        val existCount = SagerDatabase.proxyDao.countByGroup(proxyGroup.id).toInt()\n\n        if (existCount != profileMap.size) {\n            Logs.e(\"Exist profiles: $existCount, new profiles: ${profileMap.size}\")\n        }\n\n        subscription.lastUpdated = (System.currentTimeMillis() / 1000).toInt()\n        SagerDatabase.groupDao.updateGroup(proxyGroup)\n        finishUpdate(proxyGroup)\n\n        userInterface?.onUpdateSuccess(\n            proxyGroup, changed, added, updated, deleted, duplicate, byUser\n        )\n    }\n\n    fun appendExtraInfo(profile: JSONObject, bean: AbstractBean) {\n        bean.extraType = ExtraType.OOCv1\n        bean.profileId = profile.getStr(\"id\")\n        bean.group = profile.getStr(\"group\")\n        bean.owner = profile.getStr(\"owner\")\n        bean.tags = profile.getJSONArray(\"tags\")?.filterIsInstance<String>()\n    }\n\n    val supportedProtocols = arrayOf(\"shadowsocks\")\n\n    @SuppressLint(\"CustomX509TrustManager\")\n    class PinnedTrustManager(val certSha256: String) : DefaultTrustManager() {\n\n        override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {\n            val serverPK = DigestUtil.sha256Hex(chain[0].publicKey.encoded)\n            if (serverPK != certSha256) throw CertificateException(\"Excepted certSha256 $certSha256, but was $serverPK\")\n        }\n\n        override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {\n            val serverPK = DigestUtil.sha256Hex(chain[0].publicKey.encoded)\n            if (serverPK != certSha256) throw CertificateException(\"Excepted certSha256 $certSha256, but was $serverPK\")\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.group\n\nimport android.net.Uri\nimport cn.hutool.json.*\nimport com.github.shadowsocks.plugin.PluginOptions\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.*\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.gson.gson\nimport io.nekohasekai.sagernet.fmt.http.HttpBean\nimport io.nekohasekai.sagernet.fmt.hysteria.parseHysteria\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams\nimport io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.parseShadowsocksR\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean\nimport io.nekohasekai.sagernet.fmt.trojan_go.parseTrojanGo\nimport io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean\nimport io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean\nimport io.nekohasekai.sagernet.ktx.*\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport org.ini4j.Ini\nimport org.yaml.snakeyaml.TypeDescription\nimport org.yaml.snakeyaml.Yaml\nimport org.yaml.snakeyaml.error.YAMLException\nimport java.io.StringReader\n\n@Suppress(\"EXPERIMENTAL_API_USAGE\")\nobject RawUpdater : GroupUpdater() {\n\n    override suspend fun doUpdate(\n        proxyGroup: ProxyGroup,\n        subscription: SubscriptionBean,\n        userInterface: GroupManager.Interface?,\n        httpClient: OkHttpClient,\n        byUser: Boolean\n    ) {\n\n        val link = subscription.link\n        var proxies: List<AbstractBean>\n        if (link.startsWith(\"content://\")) {\n            val contentText = app.contentResolver.openInputStream(Uri.parse(link))\n                ?.bufferedReader()\n                ?.readText()\n\n            proxies = contentText?.let { parseRaw(contentText) }\n                ?: error(app.getString(R.string.no_proxies_found_in_subscription))\n        } else {\n\n            val response = httpClient.newCall(Request.Builder()\n                .url(subscription.link.toHttpUrl())\n                .header(\"User-Agent\",\n                    subscription.customUserAgent.takeIf { it.isNotBlank() }\n                        ?: \"SagerNet/${BuildConfig.VERSION_NAME}\")\n                .build()).execute().apply {\n                if (!isSuccessful) error(\"ERROR: HTTP $code\\n\\n${body?.string() ?: \"\"}\")\n                if (body == null) error(\"ERROR: Empty response\")\n            }\n\n            Logs.d(response.toString())\n            proxies = parseRaw(response.body!!.string())\n                ?: error(app.getString(R.string.no_proxies_found))\n\n        }\n\n        val proxiesMap = LinkedHashMap<String, AbstractBean>()\n        for (proxy in proxies) {\n            var index = 0\n            var name = proxy.displayName()\n            while (proxiesMap.containsKey(name)) {\n                println(\"Exists name: $name\")\n                index++\n                name = name.replace(\" (${index - 1})\", \"\")\n                name = \"$name ($index)\"\n                proxy.name = name\n            }\n            proxiesMap[proxy.displayName()] = proxy\n        }\n        proxies = proxiesMap.values.toList()\n\n        if (subscription.forceResolve) forceResolve(httpClient, proxies, proxyGroup.id)\n\n        if (subscription.forceVMessAEAD) {\n            proxies.filterIsInstance<VMessBean>().forEach { it.alterId = 0 }\n        }\n\n        val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)\n        val duplicate = ArrayList<String>()\n        if (subscription.deduplication) {\n            Logs.d(\"Before deduplication: ${proxies.size}\")\n            val uniqueProxies = LinkedHashSet<AbstractBean>()\n            val uniqueNames = HashMap<AbstractBean, String>()\n            for (proxy in proxies) {\n                if (!uniqueProxies.add(proxy)) {\n                    val index = uniqueProxies.indexOf(proxy)\n                    if (uniqueNames.containsKey(proxy)) {\n                        val name = uniqueNames[proxy]!!.replace(\" ($index)\", \"\")\n                        if (name.isNotBlank()) {\n                            duplicate.add(\"$name ($index)\")\n                            uniqueNames[proxy] = \"\"\n                        }\n                    }\n                    duplicate.add(proxy.displayName() + \" ($index)\")\n                } else {\n                    uniqueNames[proxy] = proxy.displayName()\n                }\n            }\n            uniqueProxies.retainAll(uniqueNames.keys)\n            proxies = uniqueProxies.toList()\n        }\n\n        Logs.d(\"New profiles: ${proxies.size}\")\n\n        val nameMap = proxies.associateBy { bean ->\n            bean.displayName()\n        }\n\n        Logs.d(\"Unique profiles: ${nameMap.size}\")\n\n        val toDelete = ArrayList<ProxyEntity>()\n        val toReplace = exists.mapNotNull { entity ->\n            val name = entity.displayName()\n            if (nameMap.contains(name)) name to entity else let {\n                toDelete.add(entity)\n                null\n            }\n        }.toMap()\n\n        Logs.d(\"toDelete profiles: ${toDelete.size}\")\n        Logs.d(\"toReplace profiles: ${toReplace.size}\")\n\n        val toUpdate = ArrayList<ProxyEntity>()\n        val added = mutableListOf<String>()\n        val updated = mutableMapOf<String, String>()\n        val deleted = toDelete.map { it.displayName() }\n\n        var userOrder = 1L\n        var changed = toDelete.size\n        for ((name, bean) in nameMap.entries) {\n            if (toReplace.contains(name)) {\n                val entity = toReplace[name]!!\n                val existsBean = entity.requireBean()\n                existsBean.applyFeatureSettings(bean)\n                when {\n                    existsBean != bean -> {\n                        changed++\n                        entity.putBean(bean)\n                        toUpdate.add(entity)\n                        updated[entity.displayName()] = name\n\n                        Logs.d(\"Updated profile: $name\")\n                    }\n                    entity.userOrder != userOrder -> {\n                        entity.putBean(bean)\n                        toUpdate.add(entity)\n                        entity.userOrder = userOrder\n\n                        Logs.d(\"Reordered profile: $name\")\n                    }\n                    else -> {\n                        Logs.d(\"Ignored profile: $name\")\n                    }\n                }\n            } else {\n                changed++\n                SagerDatabase.proxyDao.addProxy(ProxyEntity(\n                    groupId = proxyGroup.id, userOrder = userOrder\n                ).apply {\n                    putBean(bean)\n                })\n                added.add(name)\n                Logs.d(\"Inserted profile: $name\")\n            }\n            userOrder++\n        }\n\n        SagerDatabase.proxyDao.updateProxy(toUpdate).also {\n            Logs.d(\"Updated profiles: $it\")\n        }\n\n        SagerDatabase.proxyDao.deleteProxy(toDelete).also {\n            Logs.d(\"Deleted profiles: $it\")\n        }\n\n        val existCount = SagerDatabase.proxyDao.countByGroup(proxyGroup.id).toInt()\n\n        if (existCount != proxies.size) {\n            Logs.e(\"Exist profiles: $existCount, new profiles: ${proxies.size}\")\n        }\n\n        subscription.lastUpdated = (System.currentTimeMillis() / 1000).toInt()\n        SagerDatabase.groupDao.updateGroup(proxyGroup)\n        finishUpdate(proxyGroup)\n\n        userInterface?.onUpdateSuccess(\n            proxyGroup, changed, added, updated, deleted, duplicate, byUser\n        )\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun parseRaw(text: String): List<AbstractBean>? {\n\n        val proxies = mutableListOf<AbstractBean>()\n\n        if (text.contains(\"proxies:\")) {\n\n            try {\n\n                // clash\n                for (proxy in (Yaml().apply {\n                    addTypeDescription(TypeDescription(String::class.java, \"str\"))\n                }.loadAs(text, Map::class.java)[\"proxies\"] as? (List<Map<String, Any?>>) ?: error(\n                    app.getString(R.string.no_proxies_found_in_file)\n                ))) {\n\n                    when (proxy[\"type\"] as String) {\n                        \"socks5\" -> {\n                            proxies.add(SOCKSBean().apply {\n                                serverAddress = proxy[\"server\"] as String\n                                serverPort = proxy[\"port\"].toString().toInt()\n                                username = proxy[\"username\"] as String?\n                                password = proxy[\"password\"] as String?\n                                tls = proxy[\"tls\"]?.toString() == \"true\"\n                                sni = proxy[\"sni\"] as String?\n                                name = proxy[\"name\"] as String?\n                            })\n                        }\n                        \"http\" -> {\n                            proxies.add(HttpBean().apply {\n                                serverAddress = proxy[\"server\"] as String\n                                serverPort = proxy[\"port\"].toString().toInt()\n                                username = proxy[\"username\"] as String?\n                                password = proxy[\"password\"] as String?\n                                tls = proxy[\"tls\"]?.toString() == \"true\"\n                                sni = proxy[\"sni\"] as String?\n                                name = proxy[\"name\"] as String?\n                            })\n                        }\n                        \"ss\" -> {\n                            var pluginStr = \"\"\n                            if (proxy.contains(\"plugin\")) {\n                                val opts = PluginOptions()\n                                opts.id = proxy[\"plugin\"] as String\n                                opts.putAll(proxy[\"plugin-opts\"] as Map<String, String?>)\n                                pluginStr = opts.toString(false)\n                            }\n                            proxies.add(ShadowsocksBean().apply {\n                                serverAddress = proxy[\"server\"] as String\n                                serverPort = proxy[\"port\"].toString().toInt()\n                                password = proxy[\"password\"] as String\n                                method = proxy[\"cipher\"] as String\n                                plugin = pluginStr\n                                name = proxy[\"name\"] as String?\n\n                                fixInvalidParams()\n                            })\n                        }\n                        \"vmess\" -> {\n                            val bean = VMessBean()\n                            for (opt in proxy) {\n                                when (opt.key) {\n                                    \"name\" -> bean.name = opt.value as String\n                                    \"server\" -> bean.serverAddress = opt.value as String\n                                    \"port\" -> bean.serverPort = opt.value.toString().toInt()\n                                    \"uuid\" -> bean.uuid = opt.value as String\n                                    \"alterId\" -> bean.alterId = opt.value.toString().toInt()\n                                    \"cipher\" -> bean.encryption = opt.value as String\n                                    \"network\" -> bean.type = opt.value as String\n                                    \"tls\" -> bean.security = if (opt.value?.toString() == \"true\") \"tls\" else \"\"\n                                    \"skip-cert-verify\" -> bean.allowInsecure = opt.value == \"true\"\n                                    \"ws-path\" -> bean.path = opt.value as String\n                                    \"ws-headers\" -> for (wsHeader in (opt.value as Map<String, Any>)) {\n                                        when (wsHeader.key.lowercase()) {\n                                            \"host\" -> bean.host = wsHeader.value as String\n                                        }\n                                    }\n                                    \"ws-opts\" -> for (wsOpt in (opt.value as Map<String, Any>)) {\n                                        when (wsOpt.key.lowercase()) {\n                                            \"max-early-data\" -> {\n                                                bean.path = bean.path ?: \"\"\n                                                bean.path += \"?ed=\" + wsOpt.value\n                                            }\n                                            \"early-data-header-name\" -> {\n                                                bean.earlyDataHeaderName = wsOpt.value as String\n                                            }\n                                        }\n                                    }\n                                    \"servername\" -> bean.host = opt.value as String\n                                    \"h2-opts\" -> for (h2Opt in (opt.value as Map<String, Any>)) {\n                                        when (h2Opt.key.lowercase()) {\n                                            \"host\" -> bean.host = (h2Opt.value as List<String>).first()\n                                            \"path\" -> bean.path = h2Opt.value as String\n                                        }\n                                    }\n                                    \"http-opts\" -> for (httpOpt in (opt.value as Map<String, Any>)) {\n                                        when (httpOpt.key.lowercase()) {\n                                            \"path\" -> bean.path = (httpOpt.value as List<String>).first()\n                                        }\n                                    }\n                                    \"grpc-opts\" -> for (grpcOpt in (opt.value as Map<String, Any>)) {\n                                        when (grpcOpt.key.lowercase()) {\n                                            \"grpc-service-name\" -> bean.path = grpcOpt.value as String\n                                        }\n                                    }\n                                }\n                            }\n                            proxies.add(bean)\n                        }\n                        \"trojan\" -> {\n                            val bean = TrojanBean()\n                            for (opt in proxy) {\n                                when (opt.key) {\n                                    \"name\" -> bean.name = opt.value as String?\n                                    \"server\" -> bean.serverAddress = opt.value as String\n                                    \"port\" -> bean.serverPort = opt.value.toString().toInt()\n                                    \"password\" -> bean.password = opt.value as String\n                                    \"sni\" -> bean.sni = opt.value as String?\n                                    \"skip-cert-verify\" -> bean.allowInsecure = opt.value == \"true\"\n                                }\n                            }\n                            proxies.add(bean)\n                        }\n\n                        \"ssr\" -> {\n                            val entity = ShadowsocksRBean()\n                            for (opt in proxy) {\n                                when (opt.key) {\n                                    \"name\" -> entity.name = opt.value as String\n                                    \"server\" -> entity.serverAddress = opt.value as String\n                                    \"port\" -> entity.serverPort = opt.value.toString().toInt()\n                                    \"cipher\" -> entity.method = opt.value as String\n                                    \"password\" -> entity.password = opt.value as String\n                                    \"obfs\" -> entity.obfs = opt.value as String\n                                    \"protocol\" -> entity.protocol = opt.value as String\n                                    \"obfs-param\" -> entity.obfsParam = opt.value as String\n                                    \"protocol-param\" -> entity.protocolParam = opt.value as String\n                                }\n                            }\n                            proxies.add(entity)\n                        }\n                    }\n                }\n                proxies.forEach { it.initializeDefaultValues() }\n                return proxies\n            } catch (e: YAMLException) {\n                Logs.w(e)\n            }\n        } else if (text.contains(\"[Interface]\")) {\n            // wireguard\n            try {\n                proxies.addAll(parseWireGuard(text))\n                return proxies\n            } catch (e: Exception) {\n                Logs.w(e)\n            }\n        }\n\n        try {\n            val json = JSONUtil.parse(text)\n            return parseJSON(json)\n        } catch (ignored: JSONException) {\n        }\n\n        try {\n            return parseProxies(text.decodeBase64UrlSafe()).takeIf { it.isNotEmpty() }\n                ?: error(\"Not found\")\n        } catch (e: Exception) {\n            Logs.w(e)\n        }\n\n        try {\n            return parseProxies(text).takeIf { it.isNotEmpty() } ?: error(\"Not found\")\n        } catch (e: SubscriptionFoundException) {\n            throw e\n        } catch (ignored: Exception) {\n        }\n\n        return null\n    }\n\n    fun parseWireGuard(conf: String): List<WireGuardBean> {\n        val ini = Ini(StringReader(conf))\n        val iface = ini[\"Interface\"] ?: error(\"Missing 'Interface' selection\")\n        val bean = WireGuardBean().applyDefaultValues()\n        val localAddresses = iface.getAll(\"Address\")\n        if (localAddresses.isNullOrEmpty()) error(\"Empty address in 'Interface' selection\")\n        bean.localAddress = localAddresses.flatMap { it.split(\",\") }.let { address ->\n            address.joinToString(\"\\n\") { it.substringBefore(\"/\") }\n        }\n        bean.privateKey = iface[\"PrivateKey\"]\n        val peers = ini.getAll(\"Peer\")\n        if (peers.isNullOrEmpty()) error(\"Missing 'Peer' selections\")\n        val beans = mutableListOf<WireGuardBean>()\n        for (peer in peers) {\n            val endpoint = peer[\"Endpoint\"]\n            if (endpoint.isNullOrBlank() || !endpoint.contains(\":\")) {\n                continue\n            }\n\n            val peerBean = bean.clone()\n            peerBean.serverAddress = endpoint.substringBeforeLast(\":\")\n            peerBean.serverPort = endpoint.substringAfterLast(\":\").toIntOrNull() ?: continue\n            peerBean.peerPublicKey = peer[\"PublicKey\"] ?: continue\n            peerBean.peerPreSharedKey = peer[\"PresharedKey\"]\n            beans.add(peerBean.applyDefaultValues())\n        }\n        if (beans.isEmpty()) error(\"Empty available peer list\")\n        return beans\n    }\n\n    fun parseJSON(json: JSON): List<AbstractBean> {\n        val proxies = ArrayList<AbstractBean>()\n\n        if (json is JSONObject) {\n            when {\n                json.containsKey(\"protocol_param\") -> {\n                    return listOf(json.parseShadowsocksR())\n                }\n                json.containsKey(\"method\") -> {\n                    return listOf(json.parseShadowsocks())\n                }\n                json.containsKey(\"protocol\") -> {\n                    val v2rayConfig = gson.fromJson(\n                        json.toString(), V2RayConfig.OutboundObject::class.java\n                    ).apply { init() }\n                    return parseOutbound(v2rayConfig)\n                }\n                json.containsKey(\"outbound\") -> {\n                    val v2rayConfig = gson.fromJson(\n                        json.getJSONObject(\"outbound\").toString(),\n                        V2RayConfig.OutboundObject::class.java\n                    ).apply { init() }\n                    return parseOutbound(v2rayConfig)\n                }\n                json.containsKey(\"outbounds\") -> {/*   val fakedns = json[\"fakedns\"]\n                       if (fakedns is JSONObject) {\n                           json[\"fakedns\"] = JSONArray().apply {\n                               add(fakedns)\n                           }\n                       }\n\n                       val routing = json[\"routing\"]\n                       if (routing is JSONObject) {\n                           val rules = routing[\"rules\"]\n                           if (rules is JSONArray) {\n                               rules.filterIsInstance<JSONObject>().forEach {\n                                   val inboundTag = it[\"inboundTag\"]\n                                   if (inboundTag is String) {\n                                       it[\"inboundTag\"] = JSONArray().apply {\n                                           add(inboundTag)\n                                       }\n                                   }\n                               }\n                           }\n                       }\n\n                       try {\n                           gson.fromJson(\n                               json.toString(),\n                               V2RayConfig::class.java\n                           ).apply { init() }\n                       } catch (e: Exception) {\n                           Logs.w(e)*/\n                    json.getJSONArray(\"outbounds\").filterIsInstance<JSONObject>().forEach {\n                        val v2rayConfig = gson.fromJson(\n                            it.toString(), V2RayConfig.OutboundObject::class.java\n                        ).apply { init() }\n\n                        proxies.addAll(parseOutbound(v2rayConfig))\n                    }/* null\n                 }?.outbounds?.forEach {\n                     proxies.addAll(parseOutbound(it))\n                 }*/\n                }\n                json.containsKey(\"remote_addr\") -> {\n                    return listOf(json.parseTrojanGo())\n                }\n                json.containsKey(\"up_mbps\") -> {\n                    return listOf(json.parseHysteria())\n                }\n                else -> json.forEach { _, it ->\n                    if (it is JSON) {\n                        proxies.addAll(parseJSON(it))\n                    }\n                }\n            }\n        } else {\n            json as JSONArray\n            json.forEach {\n                if (it is JSON) {\n                    proxies.addAll(parseJSON(it))\n                }\n            }\n        }\n\n        proxies.forEach { it.initializeDefaultValues() }\n        return proxies\n    }\n\n    fun parseOutbound(outboundObject: V2RayConfig.OutboundObject): List<AbstractBean> {\n        val proxies = ArrayList<AbstractBean>()\n\n        with(outboundObject) {\n            when (protocol) {\n                \"http\" -> {\n                    val httpBean = HttpBean().applyDefaultValues()\n                    streamSettings?.apply {\n                        when (security) {\n                            \"tls\" -> {\n                                httpBean.tls = true\n                                tlsSettings?.serverName?.also {\n                                    httpBean.sni = it\n                                }\n                            }\n                        }\n                    }\n                    (settings.value as? V2RayConfig.HTTPOutboundConfigurationObject)?.servers?.forEach {\n                        val httpBeanNext = httpBean.clone().apply {\n                            serverAddress = it.address\n                            serverPort = it.port\n                        }\n                        if (it.users.isNullOrEmpty()) {\n                            proxies.add(httpBeanNext)\n                        } else for (user in it.users) proxies.add(httpBeanNext.clone().apply {\n                            username = user.user\n                            password = user.pass\n                            name = tag ?: displayName() + \" - $username\"\n                        })\n                    }\n                }\n                \"socks\" -> {\n                    val socksBean = SOCKSBean().applyDefaultValues()\n                    streamSettings?.apply {\n                        when (security) {\n                            \"tls\" -> {\n                                socksBean.tls = true\n                                tlsSettings?.serverName?.also {\n                                    socksBean.sni = it\n                                }\n                            }\n                        }\n                    }\n                    (settings.value as? V2RayConfig.SocksOutboundConfigurationObject)?.servers?.forEach {\n                        val socksBeanNext = socksBean.clone().apply {\n                            serverAddress = it.address\n                            serverPort = it.port\n                        }\n                        if (it.users.isNullOrEmpty()) {\n                            proxies.add(socksBeanNext)\n                        } else for (user in it.users) proxies.add(socksBeanNext.clone().apply {\n                            username = user.user\n                            password = user.pass\n                            name = tag ?: displayName() + \" - $username\"\n                        })\n                    }\n                }\n                \"vmess\", \"vless\" -> {\n                    val v2rayBean = (if (protocol == \"vmess\") VMessBean() else VLESSBean()).applyDefaultValues()\n                    streamSettings?.apply {\n                        v2rayBean.security = security ?: v2rayBean.security\n                        when (security) {\n                            \"tls\" -> {\n                                tlsSettings?.apply {\n                                    serverName?.also {\n                                        v2rayBean.sni = it\n                                    }\n                                    alpn?.also {\n                                        v2rayBean.alpn = it.joinToString(\",\")\n                                    }\n                                    allowInsecure?.also {\n                                        v2rayBean.allowInsecure = it\n                                    }\n                                }\n                            }\n                        }\n                        v2rayBean.type = network ?: v2rayBean.type\n                        when (network) {\n                            \"tcp\" -> {\n                                tcpSettings?.header?.apply {\n                                    when (type) {\n                                        \"http\" -> {\n                                            v2rayBean.headerType = \"http\"\n                                            request?.apply {\n                                                path?.also {\n                                                    v2rayBean.path = it.joinToString(\",\")\n                                                }\n                                                headers?.forEach { (key, value) ->\n                                                    when (key.lowercase()) {\n                                                        \"host\" -> {\n                                                            when {\n                                                                value.valueX != null -> {\n                                                                    v2rayBean.host = value.valueX\n                                                                }\n                                                                value.valueY != null -> {\n                                                                    v2rayBean.host = value.valueY.joinToString(\n                                                                        \",\"\n                                                                    )\n                                                                }\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            \"kcp\" -> {\n                                kcpSettings?.apply {\n                                    header?.type?.also {\n                                        v2rayBean.headerType = it\n                                    }\n                                    seed?.also {\n                                        v2rayBean.mKcpSeed = it\n                                    }\n                                }\n                            }\n                            \"ws\" -> {\n                                wsSettings?.apply {\n                                    headers?.forEach { (key, value) ->\n                                        when (key.lowercase()) {\n                                            \"host\" -> {\n                                                v2rayBean.host = value\n                                            }\n                                        }\n                                    }\n\n                                    path?.also {\n                                        v2rayBean.path = it\n                                    }\n\n                                    /*maxEarlyData?.also {\n                                        v2rayBean.wsMaxEarlyData = it\n                                    }*/\n                                }\n                            }\n                            \"http\", \"h2\" -> {\n                                v2rayBean.type = \"http\"\n\n                                httpSettings?.apply {\n                                    host?.also {\n                                        v2rayBean.host = it.joinToString(\",\")\n                                    }\n                                    path?.also {\n                                        v2rayBean.path = it\n                                    }\n                                }\n                            }\n                            \"quic\" -> {\n                                quicSettings?.apply {\n                                    security?.also {\n                                        v2rayBean.quicSecurity = it\n                                    }\n                                    key?.also {\n                                        v2rayBean.quicKey = it\n                                    }\n                                    header?.type?.also {\n                                        v2rayBean.headerType = it\n                                    }\n                                }\n                            }\n                            \"grpc\" -> {\n                                grpcSettings?.serviceName?.also {\n                                    v2rayBean.grpcServiceName = it\n                                }\n                            }\n                        }\n                    }\n                    if (protocol == \"vmess\") {\n                        v2rayBean as VMessBean\n                        (settings.value as? V2RayConfig.VMessOutboundConfigurationObject)?.vnext?.forEach {\n                            val vmessBean = v2rayBean.clone().apply {\n                                serverAddress = it.address\n                                serverPort = it.port\n                            }\n                            for (user in it.users) {\n                                proxies.add(vmessBean.clone().apply {\n                                    uuid = user.id\n                                    encryption = user.security\n                                    alterId = user.alterId\n                                    name = tag ?: displayName() + \" - ${user.security} - ${user.id}\"\n                                })\n                            }\n                        }\n                    } else {\n                        v2rayBean as VLESSBean\n                        (settings.value as? V2RayConfig.VLESSOutboundConfigurationObject)?.vnext?.forEach {\n                            val vlessBean = v2rayBean.clone().apply {\n                                serverAddress = it.address\n                                serverPort = it.port\n                            }\n                            for (user in it.users) {\n                                proxies.add(vlessBean.clone().apply {\n                                    uuid = user.id\n                                    encryption = user.encryption\n                                    name = tag ?: displayName() + \" - ${user.id}\"\n                                })\n                            }\n                        }\n                    }\n                }\n                \"shadowsocks\" -> (settings.value as? V2RayConfig.ShadowsocksOutboundConfigurationObject)?.servers?.forEach {\n                    proxies.add(ShadowsocksBean().applyDefaultValues().apply {\n                        name = tag\n                        serverAddress = it.address\n                        serverPort = it.port\n                        method = it.method\n                        password = it.password\n                        plugin = \"\"\n                    })\n                }\n                \"trojan\" -> {\n                    val trojanBean = TrojanBean().applyDefaultValues()\n\n                    streamSettings?.apply {\n                        trojanBean.security = security ?: trojanBean.security\n                        when (security) {\n                            \"tls\" -> {\n                                tlsSettings?.apply {\n                                    serverName?.also {\n                                        trojanBean.sni = it\n                                    }\n                                    alpn?.also {\n                                        trojanBean.alpn = it.joinToString(\",\")\n                                    }\n                                    allowInsecure?.also {\n                                        trojanBean.allowInsecure = it\n                                    }\n                                }\n                            }\n                        }\n\n                        (settings.value as? V2RayConfig.TrojanOutboundConfigurationObject)?.servers?.forEach {\n                            proxies.add(trojanBean.clone().apply {\n                                name = tag\n                                serverAddress = it.address\n                                serverPort = it.port\n                                password = it.password\n                            })\n                        }\n                    }\n                }\n            }\n            Unit\n        }\n\n        return proxies\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/group/SIP008Updater.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.group\n\nimport android.net.Uri\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.ExtraType\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.*\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.USER_AGENT\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\n\nobject SIP008Updater : GroupUpdater() {\n\n    override suspend fun doUpdate(\n        proxyGroup: ProxyGroup,\n        subscription: SubscriptionBean,\n        userInterface: GroupManager.Interface?,\n        httpClient: OkHttpClient,\n        byUser: Boolean\n    ) {\n\n        val link = subscription.link\n        val sip008Response: JSONObject\n        if (link.startsWith(\"content://\")) {\n            val contentText = app.contentResolver.openInputStream(Uri.parse(link))\n                ?.bufferedReader()\n                ?.readText()\n\n            sip008Response = contentText?.let { JSONObject(contentText) }\n                ?: error(app.getString(R.string.no_proxies_found_in_subscription))\n        } else {\n\n            val response = httpClient.newCall(Request.Builder()\n                .url(subscription.link)\n                .header(\"User-Agent\",\n                    subscription.customUserAgent.takeIf { it.isNotBlank() } ?: USER_AGENT)\n                .build()).execute().apply {\n                if (!isSuccessful) error(\"ERROR: HTTP $code\\n\\n${body?.string() ?: \"\"}\")\n                if (body == null) error(\"ERROR: Empty response\")\n            }\n\n            Logs.d(response.toString())\n\n            sip008Response = JSONObject(response.body!!.string())\n\n        }\n\n        subscription.bytesUsed = sip008Response.getLong(\"bytesUsed\", -1)\n        subscription.bytesRemaining = sip008Response.getLong(\"bytesRemaining\", -1)\n        subscription.applyDefaultValues()\n\n        val servers = sip008Response.getJSONArray(\"servers\").filterIsInstance<JSONObject>()\n\n        var profiles = mutableListOf<AbstractBean>()\n\n        for (profile in servers) {\n            val bean = profile.parseShadowsocks()\n            appendExtraInfo(profile, bean)\n            profiles.add(bean)\n        }\n\n        if (subscription.forceResolve) forceResolve(httpClient, profiles, proxyGroup.id)\n\n        val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)\n        val duplicate = ArrayList<String>()\n        if (subscription.deduplication) {\n            Logs.d(\"Before deduplication: ${profiles.size}\")\n            val uniqueProfiles = LinkedHashSet<AbstractBean>()\n            val uniqueNames = HashMap<AbstractBean, String>()\n            for (proxy in profiles) {\n                if (!uniqueProfiles.add(proxy)) {\n                    val index = uniqueProfiles.indexOf(proxy)\n                    if (uniqueNames.containsKey(proxy)) {\n                        val name = uniqueNames[proxy]!!.replace(\" ($index)\", \"\")\n                        if (name.isNotBlank()) {\n                            duplicate.add(\"$name ($index)\")\n                            uniqueNames[proxy] = \"\"\n                        }\n                    }\n                    duplicate.add(proxy.displayName() + \" ($index)\")\n                } else {\n                    uniqueNames[proxy] = proxy.displayName()\n                }\n            }\n            uniqueProfiles.retainAll(uniqueNames.keys)\n            profiles = uniqueProfiles.toMutableList()\n        }\n\n        Logs.d(\"New profiles: ${profiles.size}\")\n\n        val profileMap = profiles.associateBy { it.profileId }\n        val toDelete = ArrayList<ProxyEntity>()\n        val toReplace = exists.mapNotNull { entity ->\n            val profileId = entity.requireBean().profileId\n            if (profileMap.contains(profileId)) profileId to entity else let {\n                toDelete.add(entity)\n                null\n            }\n        }.toMap()\n\n        Logs.d(\"toDelete profiles: ${toDelete.size}\")\n        Logs.d(\"toReplace profiles: ${toReplace.size}\")\n\n        val toUpdate = ArrayList<ProxyEntity>()\n        val added = mutableListOf<String>()\n        val updated = mutableMapOf<String, String>()\n        val deleted = toDelete.map { it.displayName() }\n\n        var userOrder = 1L\n        var changed = toDelete.size\n        for ((profileId, bean) in profileMap.entries) {\n            val name = bean.displayName()\n            if (toReplace.contains(profileId)) {\n                val entity = toReplace[profileId]!!\n                val existsBean = entity.requireBean()\n                existsBean.applyFeatureSettings(bean)\n                when {\n                    existsBean != bean -> {\n                        changed++\n                        entity.putBean(bean)\n                        toUpdate.add(entity)\n                        updated[entity.displayName()] = name\n\n                        Logs.d(\"Updated profile: [$profileId] $name\")\n                    }\n                    entity.userOrder != userOrder -> {\n                        entity.putBean(bean)\n                        toUpdate.add(entity)\n                        entity.userOrder = userOrder\n\n                        Logs.d(\"Reordered profile: [$profileId] $name\")\n                    }\n                    else -> {\n                        Logs.d(\"Ignored profile: [$profileId] $name\")\n                    }\n                }\n            } else {\n                changed++\n                SagerDatabase.proxyDao.addProxy(ProxyEntity(\n                    groupId = proxyGroup.id, userOrder = userOrder\n                ).apply {\n                    putBean(bean)\n                })\n                added.add(name)\n                Logs.d(\"Inserted profile: $name\")\n            }\n            userOrder++\n        }\n\n        SagerDatabase.proxyDao.updateProxy(toUpdate).also {\n            Logs.d(\"Updated profiles: $it\")\n        }\n\n        SagerDatabase.proxyDao.deleteProxy(toDelete).also {\n            Logs.d(\"Deleted profiles: $it\")\n        }\n\n        val existCount = SagerDatabase.proxyDao.countByGroup(proxyGroup.id).toInt()\n\n        if (existCount != profileMap.size) {\n            Logs.e(\"Exist profiles: $existCount, new profiles: ${profileMap.size}\")\n        }\n\n        subscription.lastUpdated = (System.currentTimeMillis() / 1000).toInt()\n        SagerDatabase.groupDao.updateGroup(proxyGroup)\n        finishUpdate(proxyGroup)\n\n        userInterface?.onUpdateSuccess(\n            proxyGroup, changed, added, updated, deleted, duplicate, byUser\n        )\n    }\n\n    fun appendExtraInfo(profile: JSONObject, bean: AbstractBean) {\n        bean.extraType = ExtraType.SIP008\n        bean.profileId = profile.getStr(\"id\")\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n@file:Suppress(\"EXPERIMENTAL_API_USAGE\")\n\npackage io.nekohasekai.sagernet.ktx\n\nimport kotlinx.coroutines.*\n\nfun block(block: suspend CoroutineScope.() -> Unit): suspend CoroutineScope.() -> Unit {\n    return block\n}\n\nfun runOnDefaultDispatcher(block: suspend CoroutineScope.() -> Unit) =\n    GlobalScope.launch(Dispatchers.Default, block = block)\n\nsuspend fun <T> onDefaultDispatcher(block: suspend CoroutineScope.() -> T) =\n    withContext(Dispatchers.Default, block = block)\n\nfun runOnIoDispatcher(block: suspend CoroutineScope.() -> Unit) =\n    GlobalScope.launch(Dispatchers.IO, block = block)\n\nsuspend fun <T> onIoDispatcher(block: suspend CoroutineScope.() -> T) =\n    withContext(Dispatchers.IO, block = block)\n\nfun runOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) =\n    GlobalScope.launch(Dispatchers.Main.immediate, block = block)\n\nsuspend fun <T> onMainDispatcher(block: suspend CoroutineScope.() -> T) =\n    withContext(Dispatchers.Main.immediate, block = block)\n\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Browsers.kt",
    "content": "  /******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.browser.customtabs.CustomTabColorSchemeParams\nimport androidx.browser.customtabs.CustomTabsIntent\nimport io.nekohasekai.sagernet.R\n\nfun Context.launchCustomTab(link: String) {\n    CustomTabsIntent.Builder().apply {\n        setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)\n        setColorSchemeParams(\n            CustomTabsIntent.COLOR_SCHEME_LIGHT,\n            CustomTabColorSchemeParams.Builder().apply {\n                setToolbarColor(getColorAttr(R.attr.colorPrimary))\n            }.build()\n        )\n        setColorSchemeParams(\n            CustomTabsIntent.COLOR_SCHEME_DARK,\n            CustomTabColorSchemeParams.Builder().apply {\n                setToolbarColor(getColorAttr(R.attr.colorPrimary))\n            }.build()\n        )\n    }.build().launchUrl(this, Uri.parse(link))\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.content.Context\nimport androidx.appcompat.app.AlertDialog\nimport androidx.fragment.app.Fragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.nekohasekai.sagernet.R\n\nfun Context.alert(text: String): AlertDialog {\n    return MaterialAlertDialogBuilder(this).setTitle(R.string.error_title)\n        .setMessage(text)\n        .setPositiveButton(android.R.string.ok, null)\n        .create()\n}\n\nfun Fragment.alert(text: String) = requireContext().alert(text)"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Dimens.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.content.res.Resources\nimport kotlin.math.ceil\n\nprivate val density = Resources.getSystem().displayMetrics.density\n\nfun dp2pxf(dpValue: Int): Float {\n    return density * dpValue\n}\n\nfun dp2px(dpValue: Int): Int {\n    return ceil(dp2pxf(dpValue)).toInt()\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport cn.hutool.core.codec.Base64\nimport cn.hutool.json.JSONObject\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.Serializable\nimport io.nekohasekai.sagernet.fmt.brook.parseBrook\nimport io.nekohasekai.sagernet.fmt.gson.gson\nimport io.nekohasekai.sagernet.fmt.http.parseHttp\nimport io.nekohasekai.sagernet.fmt.naive.parseNaive\nimport io.nekohasekai.sagernet.fmt.parseUniversal\nimport io.nekohasekai.sagernet.fmt.pingtunnel.parsePingTunnel\nimport io.nekohasekai.sagernet.fmt.relaybaton.parseRelayBaton\nimport io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.parseShadowsocksR\nimport io.nekohasekai.sagernet.fmt.socks.parseSOCKS\nimport io.nekohasekai.sagernet.fmt.trojan.parseTrojan\nimport io.nekohasekai.sagernet.fmt.trojan_go.parseTrojanGo\nimport io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray\n\nfun formatObject(obj: Any): String {\n    return gson.toJson(obj).let { JSONObject(it).toStringPretty() }\n}\n\nfun String.decodeBase64UrlSafe(): String {\n    return Base64.decodeStr(\n        replace(' ', '-').replace('/', '_').replace('+', '-').replace(\"=\", \"\")\n    )\n}\n\nclass SubscriptionFoundException(val link: String) : RuntimeException()\n\nfun parseProxies(text: String): List<AbstractBean> {\n    val links = text.split('\\n').flatMap { it.trim().split(' ') }\n    val linksByLine = text.split('\\n').map { it.trim() }\n\n    val entities = ArrayList<AbstractBean>()\n    val entitiesByLine = ArrayList<AbstractBean>()\n\n    fun String.parseLink(entities: ArrayList<AbstractBean>) {\n        if (startsWith(\"clash://install-config?\") || startsWith(\"sn://subscription?\")) {\n            throw SubscriptionFoundException(this)\n        }\n\n        if (startsWith(\"ax://\")) {\n            Logs.d(\"Try parse universal link: $this\")\n            runCatching {\n                entities.add(parseUniversal(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"socks://\") || startsWith(\"socks4://\") || startsWith(\"socks4a://\") || startsWith(\"socks5://\")) {\n            Logs.d(\"Try parse socks link: $this\")\n            runCatching {\n                entities.add(parseSOCKS(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (matches(\"(http|https)://.*\".toRegex())) {\n            Logs.d(\"Try parse http link: $this\")\n            runCatching {\n                entities.add(parseHttp(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"vmess://\") || startsWith(\"vless://\")) {\n            Logs.d(\"Try parse v2ray link: $this\")\n            runCatching {\n                entities.add(parseV2Ray(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"trojan://\")) {\n            Logs.d(\"Try parse trojan link: $this\")\n            runCatching {\n                entities.add(parseTrojan(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"trojan-go://\")) {\n            Logs.d(\"Try parse trojan-go link: $this\")\n            runCatching {\n                entities.add(parseTrojanGo(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"ss://\")) {\n            Logs.d(\"Try parse shadowsocks link: $this\")\n            runCatching {\n                entities.add(parseShadowsocks(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"ssr://\")) {\n            Logs.d(\"Try parse shadowsocksr link: $this\")\n            runCatching {\n                entities.add(parseShadowsocksR(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"naive+\")) {\n            Logs.d(\"Try parse naive link: $this\")\n            runCatching {\n                entities.add(parseNaive(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"ping-tunnel://\")) {\n            Logs.d(\"Try parse pt link: $this\")\n            runCatching {\n                entities.add(parsePingTunnel(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"relaybaton://\")) {\n            Logs.d(\"Try parse rb link: $this\")\n            runCatching {\n                entities.add(parseRelayBaton(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else if (startsWith(\"brook://\")) {\n            Logs.d(\"Try parse brook link: $this\")\n            runCatching {\n                entities.add(parseBrook(this))\n            }.onFailure {\n                Logs.w(it)\n            }\n        }\n    }\n\n    for (link in links) {\n        link.parseLink(entities)\n    }\n    for (link in linksByLine) {\n        link.parseLink(entitiesByLine)\n    }\n    var isBadLink = false\n    if (entities.onEach { it.initializeDefaultValues() }.size == entitiesByLine.onEach { it.initializeDefaultValues() }.size) run test@{\n        entities.forEachIndexed { index, bean ->\n            val lineBean = entitiesByLine[index]\n            if (bean == lineBean && bean.displayName() != lineBean.displayName()) {\n                isBadLink = true\n                return@test\n            }\n        }\n    }\n    return if (entities.size > entitiesByLine.size) entities else entitiesByLine\n}\n\nfun <T : Serializable> T.applyDefaultValues(): T {\n    initializeDefaultValues()\n    return this\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Kryos.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.os.Parcel\nimport android.os.Parcelable\nimport com.esotericsoftware.kryo.io.ByteBufferInput\nimport com.esotericsoftware.kryo.io.ByteBufferOutput\nimport java.io.InputStream\nimport java.io.OutputStream\n\n\nfun InputStream.byteBuffer() = ByteBufferInput(this)\nfun OutputStream.byteBuffer() = ByteBufferOutput(this)\n\nfun ByteBufferInput.readStringList(): List<String> {\n    return mutableListOf<String>().apply {\n        repeat(readInt()) {\n            add(readString())\n        }\n    }\n}\n\nfun ByteBufferInput.readStringSet(): Set<String> {\n    return linkedSetOf<String>().apply {\n        repeat(readInt()) {\n            add(readString())\n        }\n    }\n}\n\n\nfun ByteBufferOutput.writeStringList(list: List<String>) {\n    writeInt(list.size)\n    for (str in list) writeString(str)\n}\n\nfun ByteBufferOutput.writeStringList(list: Set<String>) {\n    writeInt(list.size)\n    for (str in list) writeString(str)\n}\n\nfun Parcelable.marshall(): ByteArray {\n    val parcel = Parcel.obtain()\n    writeToParcel(parcel, 0)\n    val bytes = parcel.marshall()\n    parcel.recycle()\n    return bytes\n}\n\nfun ByteArray.unmarshall(): Parcel {\n    val parcel = Parcel.obtain()\n    parcel.unmarshall(this, 0, size)\n    parcel.setDataPosition(0) // This is extremely important!\n    return parcel\n}\n\nfun <T> ByteArray.unmarshall(constructor: (Parcel) -> T): T {\n    val parcel = unmarshall()\n    val result = constructor(parcel)\n    parcel.recycle()\n    return result\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Layouts.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.graphics.Rect\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport io.nekohasekai.sagernet.ui.MainActivity\n\nclass FixedLinearLayoutManager(val recyclerView: RecyclerView) :\n    LinearLayoutManager(recyclerView.context, RecyclerView.VERTICAL, false) {\n\n    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {\n        try {\n            super.onLayoutChildren(recycler, state)\n        } catch (ignored: IndexOutOfBoundsException) {\n        }\n    }\n\n    private var listenerDisabled = false\n\n    override fun scrollVerticallyBy(\n        dx: Int, recycler: RecyclerView.Recycler,\n        state: RecyclerView.State\n    ): Int {\n        val scrollRange = super.scrollVerticallyBy(dx, recycler, state)\n        if (listenerDisabled) return scrollRange\n        val activity = recyclerView.context as? MainActivity\n        if (activity == null) {\n            listenerDisabled = true\n            return scrollRange\n        }\n\n        val overscroll = dx - scrollRange\n        if (overscroll > 0) {\n            val view =\n                (recyclerView.findViewHolderForAdapterPosition(findLastVisibleItemPosition())\n                    ?: return scrollRange).itemView\n            val itemLocation = Rect().also { view.getGlobalVisibleRect(it) }\n            val fabLocation = Rect().also { activity.binding.fab.getGlobalVisibleRect(it) }\n            if (!itemLocation.contains(fabLocation.left, fabLocation.top) && !itemLocation.contains(fabLocation.right, fabLocation.bottom)) {\n                return scrollRange\n            }\n            activity.binding.fab.apply {\n                if (isShown) hide()\n            }\n        } else {\n            /*val screen = Rect().also { activity.window.decorView.getGlobalVisibleRect(it) }\n            val location = Rect().also { activity.stats.getGlobalVisibleRect(it) }\n            if (screen.bottom < location.bottom) {\n                return scrollRange\n            }\n            val height = location.bottom - location.top\n            val mH = activity.stats.measuredHeight\n\n            if (mH > height) {\n                return scrollRange\n            }*/\n\n            activity.binding.fab.apply {\n                if (!isShown) show()\n            }\n        }\n        return scrollRange\n    }\n\n}\n\nclass FixedGridLayoutManager(val recyclerView: RecyclerView, spanCount: Int) :\n    GridLayoutManager(recyclerView.context, spanCount) {\n\n    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {\n        try {\n            super.onLayoutChildren(recycler, state)\n        } catch (ignored: IndexOutOfBoundsException) {\n        }\n    }\n\n    private var listenerDisabled = false\n\n    override fun scrollVerticallyBy(\n        dx: Int, recycler: RecyclerView.Recycler,\n        state: RecyclerView.State\n    ): Int {\n        val scrollRange = super.scrollVerticallyBy(dx, recycler, state)\n        if (listenerDisabled) return scrollRange\n        val activity = recyclerView.context as? MainActivity\n        if (activity == null) {\n            listenerDisabled = true\n            return scrollRange\n        }\n\n        val overscroll = dx - scrollRange\n        if (overscroll > 0) {\n            val view =\n                (recyclerView.findViewHolderForAdapterPosition(findLastVisibleItemPosition())\n                    ?: return scrollRange).itemView\n            val itemLocation = Rect().also { view.getGlobalVisibleRect(it) }\n            val fabLocation = Rect().also { activity.binding.fab.getGlobalVisibleRect(it) }\n            if (!itemLocation.contains(fabLocation.left, fabLocation.top) && !itemLocation.contains(fabLocation.right, fabLocation.bottom)) {\n                return scrollRange\n            }\n            activity.binding.fab.apply {\n                if (isShown) hide()\n            }\n        } else {\n            /*val screen = Rect().also { activity.window.decorView.getGlobalVisibleRect(it) }\n            val location = Rect().also { activity.stats.getGlobalVisibleRect(it) }\n            if (screen.bottom < location.bottom) {\n                return scrollRange\n            }\n            val height = location.bottom - location.top\n            val mH = activity.stats.measuredHeight\n\n            if (mH > height) {\n                return scrollRange\n            }*/\n\n            activity.binding.fab.apply {\n                if (!isShown) show()\n            }\n        }\n        return scrollRange\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Logs.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.util.Log\nimport cn.hutool.core.util.StrUtil\nimport java.io.InputStream\nimport java.io.OutputStream\n\nobject Logs {\n\n    private fun mkTag(): String {\n        val stackTrace = Thread.currentThread().stackTrace\n        return StrUtil.subAfter(stackTrace[4].className, \".\", true)\n    }\n\n    fun v(message: String) {\n        //  if (BuildConfig.DEBUG) {\n        Log.v(mkTag(), message)\n//        }\n    }\n\n    fun v(message: String, exception: Throwable) {\n        //  if (BuildConfig.DEBUG) {\n        Log.v(mkTag(), message, exception)\n//        }\n    }\n\n    fun d(message: String) {\n        //  if (BuildConfig.DEBUG) {\n        Log.d(mkTag(), message)\n//        }\n    }\n\n    fun d(message: String, exception: Throwable) {\n        //  if (BuildConfig.DEBUG) {\n        Log.d(mkTag(), message, exception)\n//        }\n    }\n\n    fun i(message: String) {\n        Log.i(mkTag(), message)\n    }\n\n    fun i(message: String, exception: Throwable) {\n        Log.i(mkTag(), message, exception)\n    }\n\n    fun w(message: String) {\n        Log.w(mkTag(), message)\n    }\n\n    fun w(message: String, exception: Throwable) {\n        Log.w(mkTag(), message, exception)\n    }\n\n    fun w(exception: Throwable) {\n        Log.w(mkTag(), exception)\n    }\n\n    fun e(message: String) {\n        Log.e(mkTag(), message)\n    }\n\n    fun e(message: String, exception: Throwable) {\n        Log.e(mkTag(), message, exception)\n    }\n\n}\n\nfun InputStream.use(out: OutputStream) {\n    use { input ->\n        out.use { output ->\n            input.copyTo(output)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n@file:Suppress(\"SpellCheckingInspection\")\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.os.Build\nimport cn.hutool.core.lang.Validator\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.bg.VpnService\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.LOCALHOST\nimport okhttp3.ConnectionSpec\nimport okhttp3.HttpUrl\nimport okhttp3.OkHttpClient\nimport java.net.InetAddress\nimport java.net.InetSocketAddress\nimport java.net.Proxy\nimport java.net.Socket\n\nval okHttpClient = OkHttpClient.Builder()\n    .followRedirects(true)\n    .followSslRedirects(true)\n    .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.RESTRICTED_TLS))\n    .build()\n\nprivate lateinit var proxyClient: OkHttpClient\nfun createProxyClient(): OkHttpClient {\n    if (!SagerNet.started) return okHttpClient\n\n    if (!::proxyClient.isInitialized) {\n        proxyClient = okHttpClient.newBuilder().proxy(requireProxy()).build()\n    }\n    return proxyClient\n}\n\n\nfun requireProxy(): Proxy {\n    return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {\n        Proxy(Proxy.Type.SOCKS, InetSocketAddress(LOCALHOST, DataStore.socksPort))\n    } else {\n        Proxy(Proxy.Type.HTTP, InetSocketAddress(LOCALHOST, DataStore.httpPort))\n    }\n}\n\nfun linkBuilder() = HttpUrl.Builder().scheme(\"https\")\n\nfun HttpUrl.Builder.toLink(scheme: String, appendDefaultPort: Boolean = true): String {\n    var url = build()\n    val defaultPort = HttpUrl.defaultPort(url.scheme)\n    var replace = false\n    if (appendDefaultPort && url.port == defaultPort) {\n        url = url.newBuilder().port(14514).build()\n        replace = true\n    }\n    return url.toString().replace(\"${url.scheme}://\", \"$scheme://\").let {\n        if (replace) it.replace(\":14514\", \":$defaultPort\") else it\n    }\n}\n\nfun String.isIpAddress(): Boolean {\n    return Validator.isIpv4(this) || Validator.isIpv6(this)\n}\n\nfun String.unwrapHost(): String {\n    if (startsWith(\"[\") && endsWith(\"]\")) {\n        return substring(1, length - 1).unwrapHost()\n    }\n    return this\n}\n\nfun AbstractBean.wrapUri(): String {\n    return if (Validator.isIpv6(finalAddress)) {\n        \"[$finalAddress]:$finalPort\"\n    } else {\n        \"$finalAddress:$finalPort\"\n    }\n}\n\nfun parseAddress(addressArray: ByteArray) = InetAddress.getByAddress(addressArray)\nval INET_TUN = InetAddress.getByName(VpnService.PRIVATE_VLAN4_CLIENT)\nval INET6_TUN = InetAddress.getByName(VpnService.PRIVATE_VLAN6_CLIENT)\n\nfun mkPort(): Int {\n    val socket = Socket()\n    socket.reuseAddress = true\n    socket.bind(InetSocketAddress(0))\n    val port = socket.localPort\n    socket.close()\n    return port\n}\n\nconst val IPPROTO_ICMP = 1\nconst val IPPROTO_ICMPv6 = 58\n\nconst val IPPROTO_TCP = 6\nconst val IPPROTO_UDP = 17\n\nconst val USER_AGENT = \"curl/7.74.0\"\nconst val USER_AGENT_ORIGIN = \"SagerNet/${BuildConfig.VERSION_NAME}\""
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Preferences.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport androidx.preference.PreferenceDataStore\nimport cn.hutool.core.util.NumberUtil\nimport kotlin.reflect.KProperty\n\nfun PreferenceDataStore.string(\n    name: String,\n    defaultValue: () -> String = { \"\" },\n) = PreferenceProxy(name, defaultValue, ::getString, ::putString)\n\nfun PreferenceDataStore.boolean(\n    name: String,\n    defaultValue: () -> Boolean = { false },\n) = PreferenceProxy(name, defaultValue, ::getBoolean, ::putBoolean)\n\nfun PreferenceDataStore.int(\n    name: String,\n    defaultValue: () -> Int = { 0 },\n) = PreferenceProxy(name, defaultValue, ::getInt, ::putInt)\n\nfun PreferenceDataStore.stringToInt(\n    name: String,\n    defaultValue: () -> Int = { 0 },\n) = PreferenceProxy(name, defaultValue, { key, default ->\n    getString(key, \"$default\")?.takeIf { NumberUtil.isInteger(it) }?.toInt() ?: default\n}, { key, value -> putString(key, \"$value\") })\n\nfun PreferenceDataStore.stringToIntIfExists(\n    name: String,\n    defaultValue: () -> Int = { 0 },\n) = PreferenceProxy(name, defaultValue, { key, default ->\n    getString(key, \"$default\")?.takeIf { NumberUtil.isInteger(it) }?.toInt() ?: default\n}, { key, value -> putString(key, value.takeIf { it > 0 }?.toString() ?: \"\") })\n\nfun PreferenceDataStore.long(\n    name: String,\n    defaultValue: () -> Long = { 0L },\n) = PreferenceProxy(name, defaultValue, ::getLong, ::putLong)\n\nfun PreferenceDataStore.stringToLong(\n    name: String,\n    defaultValue: () -> Long = { 0L },\n) = PreferenceProxy(name, defaultValue, { key, default ->\n    getString(key, \"$default\")?.takeIf { NumberUtil.isLong(it) }?.toLong() ?: default\n}, { key, value -> putString(key, \"$value\") })\n\nclass PreferenceProxy<T>(\n    val name: String,\n    val defaultValue: () -> T,\n    val getter: (String, T) -> T?,\n    val setter: (String, value: T) -> Unit,\n) {\n\n    operator fun setValue(thisObj: Any?, property: KProperty<*>, value: T) = setter(name, value)\n    operator fun getValue(thisObj: Any?, property: KProperty<*>) = getter(name, defaultValue())!!\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Signatures.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n/***\n * If you modify and release but do not release the source code, you violate the GPL, so this is made.\n *\n * @author nekohasekai\n */\npackage io.nekohasekai.sagernet.ktx\n\nimport android.content.Context\nimport android.content.pm.PackageManager.GET_SIGNATURES\nimport android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES\nimport android.content.pm.Signature\nimport android.os.Build\nimport android.os.Process\nimport cn.hutool.crypto.digest.DigestUtil\n\nval devKeys = arrayOf(\n    \"32250A4B5F3A6733DF57A3B9EC16C38D2C7FC5F2F693A9636F8F7B3BE3549641\"\n)\n\nfun Context.getSignature(): Signature {\n    val appInfo = packageManager.getPackageInfo(\n        packageName, if (Build.VERSION.SDK_INT >= 28) GET_SIGNING_CERTIFICATES else GET_SIGNATURES\n    )\n    return if (Build.VERSION.SDK_INT >= 28) {\n        appInfo.signingInfo.apkContentsSigners[0]\n    } else {\n        appInfo.signatures[0]\n    }\n}\n\nfun Context.getSha256Signature(): String {\n    return DigestUtil.sha256Hex(getSignature().toByteArray()).uppercase()\n}\n\nfun Context.isVerified(): Boolean {\n    when (val s = getSha256Signature()) {\n        in devKeys,\n        -> return true\n        else -> {\n            Logs.w(\"Unknown signature: $s\")\n        }\n    }\n    return false\n}\n\nfun Context.checkMT() {\n    val fuckMT = block {\n        Thread.setDefaultUncaughtExceptionHandler(null)\n        Thread.currentThread().uncaughtExceptionHandler = null\n        try {\n            Process.killProcess(Process.myPid())\n        } catch (e: Exception) {\n        }\n        Runtime.getRuntime().exit(0)\n    }\n\n    try {\n        Class.forName(\"bin.mt.apksignaturekillerplus.HookApplication\")\n        runOnMainDispatcher(fuckMT)\n        return\n    } catch (ignored: ClassNotFoundException) {\n    }\n\n    if (isVerified()) return\n\n    val manifestMF = javaClass.getResourceAsStream(\"/META-INF/MANIFEST.MF\")\n    if (manifestMF == null) {\n        Logs.w(\"/META-INF/MANIFEST.MF not found\")\n        return\n    }\n\n    val input = manifestMF.bufferedReader()\n    val headers = input.use { (0 until 5).map { readLine() } }.joinToString(\"\\n\")\n\n    // WTF version?\n    if (headers.contains(\"Android Gradle 3.5.0\")) {\n        runOnMainDispatcher(fuckMT)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/UUIDs.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport cn.hutool.core.lang.UUID\nimport cn.hutool.core.util.ArrayUtil\nimport cn.hutool.crypto.digest.DigestUtil\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport kotlin.experimental.and\nimport kotlin.experimental.or\n\nfun uuid5(text: String): String {\n    val data = ByteArrayOutputStream()\n    data.write(ByteArray(16))\n    data.write(text.toByteArray())\n    val hash = DigestUtil.sha1(ByteArrayInputStream(data.toByteArray()))\n    val result = ArrayUtil.sub(hash, 0, 16)\n    result[6] = result[6] and 0x0F.toByte()\n    result[6] = result[6] or 0x50.toByte()\n    result[8] = result[8] and 0x3F.toByte()\n    result[8] = result[8] or 0x80.toByte()\n    var msb = 0L\n    for (i in 0..7) {\n        msb = msb shl 8 or (result[i].toLong() and 0xff)\n    }\n    var lsb = 0L\n    for (i in 8..15) {\n        lsb = lsb shl 8 or (result[i].toLong() and 0xff)\n    }\n    return UUID(msb, lsb).toString(false)\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\n@file:SuppressLint(\"SoonBlockedPrivateApi\")\n\npackage io.nekohasekai.sagernet.ktx\n\nimport android.animation.Animator\nimport android.animation.AnimatorListenerAdapter\nimport android.annotation.SuppressLint\nimport android.app.Service\nimport android.content.*\nimport android.content.pm.PackageInfo\nimport android.content.res.Resources\nimport android.net.NetworkUtils\nimport android.os.Build\nimport android.os.SystemClock\nimport android.system.Os\nimport android.system.OsConstants\nimport android.util.TypedValue\nimport android.view.View\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.annotation.AttrRes\nimport androidx.annotation.ColorRes\nimport androidx.core.content.ContextCompat\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentManager\nimport androidx.preference.Preference\nimport androidx.recyclerview.widget.LinearSmoothScroller\nimport androidx.recyclerview.widget.RecyclerView\nimport cn.hutool.core.net.URLDecoder\nimport cn.hutool.core.net.URLEncoder\nimport cn.hutool.core.util.CharsetUtil\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.ui.MainActivity\nimport io.nekohasekai.sagernet.ui.ThemedActivity\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport sun.misc.Unsafe\nimport java.io.FileDescriptor\nimport java.net.HttpURLConnection\nimport java.net.InetAddress\nimport java.net.Socket\nimport java.util.concurrent.atomic.AtomicBoolean\nimport java.util.concurrent.atomic.AtomicInteger\nimport java.util.concurrent.atomic.AtomicLong\nimport java.util.concurrent.atomic.AtomicReference\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\nimport kotlin.reflect.KClass\nimport kotlin.reflect.KMutableProperty0\nimport kotlin.reflect.KProperty\nimport kotlin.reflect.KProperty0\n\n\ninline fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) {\n    var result: Exception? = null\n    for (element in this) try {\n        action(element)\n    } catch (e: Exception) {\n        if (result == null) result = e else result.addSuppressed(e)\n    }\n    if (result != null) {\n        throw result\n    }\n}\n\nval Throwable.readableMessage\n    get() = localizedMessage.takeIf { !it.isNullOrBlank() } ?: javaClass.simpleName\n\n/**\n * https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466\n */\n\nprivate val socketGetFileDescriptor = Socket::class.java.getDeclaredMethod(\"getFileDescriptor\\$\")\nval Socket.fileDescriptor get() = socketGetFileDescriptor.invoke(this) as FileDescriptor\n\nprivate val getInt = FileDescriptor::class.java.getDeclaredMethod(\"getInt$\")\nval FileDescriptor.int get() = getInt.invoke(this) as Int\n\nsuspend fun <T> HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T {\n    return suspendCancellableCoroutine { cont ->\n        cont.invokeOnCancellation {\n            if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }\n        }\n        GlobalScope.launch(Dispatchers.IO) {\n            try {\n                cont.resume(block())\n            } catch (e: Throwable) {\n                cont.resumeWithException(e)\n            }\n        }\n    }\n}\n\nfun parsePort(str: String?, default: Int, min: Int = 1025): Int {\n    val value = str?.toIntOrNull() ?: default\n    return if (value < min || value > 65535) default else value\n}\n\nfun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =\n    object : BroadcastReceiver() {\n        override fun onReceive(context: Context, intent: Intent) = callback(context, intent)\n    }\n\nfun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =\n    object : BroadcastReceiver() {\n        override fun onReceive(context: Context, intent: Intent) {\n            callback()\n            if (onetime) context.unregisterReceiver(this)\n        }\n    }.apply {\n        registerReceiver(this, IntentFilter().apply {\n            addAction(Intent.ACTION_PACKAGE_ADDED)\n            addAction(Intent.ACTION_PACKAGE_REMOVED)\n            addDataScheme(\"package\")\n        })\n    }\n\nval PackageInfo.signaturesCompat\n    get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress(\"DEPRECATION\") signatures\n\n/**\n * Based on: https://stackoverflow.com/a/26348729/2245107\n */\nfun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {\n    val typedValue = TypedValue()\n    if (!resolveAttribute(resId, typedValue, true)) throw Resources.NotFoundException()\n    return typedValue.resourceId\n}\n\nfun Preference.remove() = parent!!.removePreference(this)\n\n/**\n * A slightly more performant variant of parseNumericAddress.\n *\n * Bug in Android 9.0 and lower: https://issuetracker.google.com/issues/123456213\n */\n\nprivate val parseNumericAddress by lazy {\n    InetAddress::class.java.getDeclaredMethod(\"parseNumericAddress\", String::class.java).apply {\n        isAccessible = true\n    }\n}\n\nfun String?.parseNumericAddress(): InetAddress? =\n    Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let {\n        if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(\n            null, this\n        ) as InetAddress\n    }\n\n@JvmOverloads\nfun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String? = null) {\n    if (!fragmentManager.isStateSaved) show(fragmentManager, tag)\n}\n\nprivate val encoder = URLEncoder().apply {\n\n    addSafeCharacter('*')\n    addSafeCharacter('-')\n    addSafeCharacter('.')\n    addSafeCharacter('_')\n    addSafeCharacter('&')\n    addSafeCharacter('/')\n\n}\n\nfun String.pathSafe(): String {\n    return encoder.encode(this, CharsetUtil.CHARSET_UTF_8)\n}\n\nfun String.urlSafe(): String {\n    return URLEncoder.ALL.encode(this, CharsetUtil.CHARSET_UTF_8)\n}\n\nfun String.unUrlSafe(): String {\n    return URLDecoder.decode(this, CharsetUtil.CHARSET_UTF_8)\n}\n\nfun RecyclerView.scrollTo(index: Int, force: Boolean = false) {\n    if (force) post {\n        scrollToPosition(index)\n    }\n    postDelayed({\n        try {\n            layoutManager?.startSmoothScroll(object : LinearSmoothScroller(context) {\n                init {\n                    targetPosition = index\n                }\n\n                override fun getVerticalSnapPreference(): Int {\n                    return SNAP_TO_START\n                }\n            })\n        } catch (ignored: IllegalArgumentException) {\n        }\n    }, 300L)\n}\n\nval app get() = SagerNet.application\n\nval shortAnimTime by lazy {\n    app.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()\n}\n\nfun View.crossFadeFrom(other: View) {\n    clearAnimation()\n    other.clearAnimation()\n    if (visibility == View.VISIBLE && other.visibility == View.GONE) return\n    alpha = 0F\n    visibility = View.VISIBLE\n    animate().alpha(1F).duration = shortAnimTime\n    other.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {\n        override fun onAnimationEnd(animation: Animator) {\n            other.visibility = View.GONE\n        }\n    }).duration = shortAnimTime\n}\n\n\nfun Fragment.snackbar(textId: Int) = (requireActivity() as MainActivity).snackbar(textId)\nfun Fragment.snackbar(text: CharSequence) = (requireActivity() as MainActivity).snackbar(text)\n\nfun ThemedActivity.startFilesForResult(\n    launcher: ActivityResultLauncher<String>, input: String\n) {\n    try {\n        return launcher.launch(input)\n    } catch (_: ActivityNotFoundException) {\n    } catch (_: SecurityException) {\n    }\n    snackbar(getString(R.string.file_manager_missing)).show()\n}\n\nfun Fragment.startFilesForResult(\n    launcher: ActivityResultLauncher<String>, input: String\n) {\n    try {\n        return launcher.launch(input)\n    } catch (_: ActivityNotFoundException) {\n    } catch (_: SecurityException) {\n    }\n    (requireActivity() as ThemedActivity).snackbar(getString(R.string.file_manager_missing)).show()\n}\n\nfun Fragment.needReload() {\n    if (SagerNet.started) {\n        snackbar(getString(R.string.restart)).setAction(R.string.apply) {\n            SagerNet.reloadService()\n        }.show()\n    }\n}\n\n@Suppress(\"DEPRECATION\")\nfun <T : Service> KClass<T>.isRunning(): Boolean {\n    val name = qualifiedName\n    var myServices = SagerNet.activity.getRunningServices(5) ?: return false\n    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n        val myUid = Os.getuid()\n        myServices = myServices.filter { it.uid == myUid }\n    }\n    for (myService in myServices) if (myService.service.className == name) {\n        return true\n    }\n    return false\n}\n\n\nfun Context.getColour(@ColorRes colorRes: Int): Int {\n    return ContextCompat.getColor(this, colorRes)\n}\n\nfun Context.getColorAttr(@AttrRes resId: Int): Int {\n    return ContextCompat.getColor(this, TypedValue().also {\n        theme.resolveAttribute(resId, it, true)\n    }.resourceId)\n}\n\nconst val isDefaultFlavor = BuildConfig.FLAVOR == \"oss\"\nconst val isExpert = BuildConfig.FLAVOR == \"expert\"\n\nval LAUNCH_DELAY = System.currentTimeMillis() - SystemClock.elapsedRealtime()\n\nprivate val protectDirectAvailable by lazy {\n    try {\n        NetworkUtils::class.java.getDeclaredMethod(\"protectFromVpn\", Int::class.java)\n        true\n    } catch (e: Exception) {\n        false\n    }\n}\n\nfun Fragment.protectFromVpn(fd: Int) {\n    if (protectDirectAvailable) {\n        NetworkUtils.protectFromVpn(fd)\n    } else {\n        (requireActivity() as? MainActivity)?.connection?.service?.protect(fd)\n    }\n}\n\nfun <T> Continuation<T>.tryResume(value: T) {\n    try {\n        resumeWith(Result.success(value))\n    } catch (ignored: IllegalStateException) {\n    }\n}\n\nfun <T> Continuation<T>.tryResumeWithException(exception: Throwable) {\n    try {\n        resumeWith(Result.failure(exception))\n    } catch (ignored: IllegalStateException) {\n    }\n}\n\noperator fun <F> KProperty0<F>.getValue(thisRef: Any?, property: KProperty<*>): F = get()\noperator fun <F> KMutableProperty0<F>.setValue(\n    thisRef: Any?, property: KProperty<*>, value: F\n) = set(value)\n\noperator fun AtomicBoolean.getValue(thisRef: Any?, property: KProperty<*>): Boolean = get()\noperator fun AtomicBoolean.setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) =\n    set(value)\n\noperator fun AtomicInteger.getValue(thisRef: Any?, property: KProperty<*>): Int = get()\noperator fun AtomicInteger.setValue(thisRef: Any?, property: KProperty<*>, value: Int) = set(value)\n\noperator fun AtomicLong.getValue(thisRef: Any?, property: KProperty<*>): Long = get()\noperator fun AtomicLong.setValue(thisRef: Any?, property: KProperty<*>, value: Long) = set(value)\n\noperator fun <T> AtomicReference<T>.getValue(thisRef: Any?, property: KProperty<*>): T = get()\noperator fun <T> AtomicReference<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) =\n    set(value)\n\noperator fun <K, V> Map<K, V>.getValue(thisRef: K, property: KProperty<*>) = get(thisRef)\noperator fun <K, V> MutableMap<K, V>.setValue(thisRef: K, property: KProperty<*>, value: V?) {\n\n    if (value != null) {\n\n        put(thisRef, value)\n\n    } else {\n\n        remove(thisRef)\n\n    }\n\n}\n\n@SuppressLint(\"DiscouragedPrivateApi\")\nval UNSAFE = try {\n    Unsafe::class.java.getDeclaredMethod(\"getUnsafe\").invoke(null) as Unsafe?\n} catch (e: Throwable) {\n    null\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ktx/Validators.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport androidx.annotation.RawRes\nimport cn.hutool.core.lang.Validator\nimport cn.hutool.core.net.NetUtil.isInnerIP\nimport cn.hutool.json.JSONObject\nimport com.github.shadowsocks.plugin.PluginConfiguration\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.http.HttpBean\nimport io.nekohasekai.sagernet.fmt.internal.ConfigBean\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean\nimport io.nekohasekai.sagernet.group.RawUpdater\n\ninterface ValidateResult\nobject ResultSecure : ValidateResult\nobject ResultLocal : ValidateResult\nclass ResultDeprecated(@RawRes val textRes: Int) : ValidateResult\nclass ResultInsecure(@RawRes val textRes: Int) : ValidateResult\n\nval ssSecureList = \"(gcm|poly1305)\".toRegex()\n\nfun AbstractBean.isInsecure(): ValidateResult {\n    if (Validator.isIpv4(serverAddress) && isInnerIP(serverAddress) || serverAddress in arrayOf(\n            \"localhost\", \"::\"\n        )\n    ) {\n        return ResultLocal\n    }\n    if (this is ShadowsocksBean) {\n        if (plugin.isBlank() || PluginConfiguration(plugin).selected == \"obfs-local\") {\n            if (!method.contains(ssSecureList)) {\n                return ResultInsecure(R.raw.shadowsocks_stream_cipher)\n            }\n        }\n    } else if (this is ShadowsocksRBean) {\n        return ResultDeprecated(R.raw.shadowsocksr)\n    } else if (this is HttpBean) {\n        if (!tls) return ResultInsecure(R.raw.not_encrypted)\n    } else if (this is SOCKSBean) {\n        if (!tls) return ResultInsecure(R.raw.not_encrypted)\n    } else if (this is VMessBean) {\n        if (security in arrayOf(\"\", \"none\")) {\n            if (encryption in arrayOf(\"none\", \"zero\")) {\n                return ResultInsecure(R.raw.not_encrypted)\n            }\n        }\n        if (type == \"kcp\" && mKcpSeed.isBlank()) {\n            return ResultInsecure(R.raw.mkcp_no_seed)\n        }\n        if (allowInsecure) return ResultInsecure(R.raw.insecure)\n        if (alterId > 0) return ResultDeprecated(R.raw.vmess_md5_auth)\n    } else if (this is VLESSBean) {\n        if (security in arrayOf(\"\", \"none\")) {\n            return ResultInsecure(R.raw.not_encrypted)\n        }\n        if (type == \"kcp\" && mKcpSeed.isBlank()) {\n            return ResultInsecure(R.raw.mkcp_no_seed)\n        }\n        if (allowInsecure) return ResultInsecure(R.raw.insecure)\n    } else if (this is ConfigBean) {\n        try {\n            val profiles = RawUpdater.parseJSON(JSONObject(content))\n            val results = profiles.map { it.isInsecure() }\n            (results.find { it is ResultInsecure } ?: results.find { it is ResultDeprecated }\n            ?: results.find { it is ResultLocal })?.also {\n                return it\n            }\n        } catch (ignored: Exception) {\n        }\n    }\n    return ResultSecure\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/plugin/NativePlugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.content.pm.ResolveInfo\n\nclass NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {\n    init {\n        check(resolveInfo.providerInfo != null)\n    }\n\n    override val componentInfo get() = resolveInfo.providerInfo!!\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/plugin/Plugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.graphics.drawable.Drawable\n\nabstract class Plugin {\n    abstract val id: String\n    abstract val label: CharSequence\n    abstract val version: Int\n    abstract val versionName: String\n    open val icon: Drawable? get() = null\n    open val defaultConfig: String? get() = null\n    open val packageName: String get() = \"\"\n    open val directBootAware: Boolean get() = true\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n        return id == (other as Plugin).id\n    }\n\n    override fun hashCode() = id.hashCode()\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/plugin/PluginList.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport io.nekohasekai.sagernet.SagerNet\n\nclass PluginList : ArrayList<Plugin>() {\n    init {\n        addAll(SagerNet.application.packageManager.queryIntentContentProviders(\n            Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)\n            .filter { it.providerInfo.exported }.map { NativePlugin(it) })\n    }\n\n    val lookup = mutableMapOf<String, Plugin>().apply {\n        for (plugin in this@PluginList.toList()) {\n            fun check(old: Plugin?) {\n                if (old != null && old != plugin) {\n                    this@PluginList.remove(old)\n                }\n               /* if (old != null && old !== plugin) {\n                    val packages = this@PluginList.filter { it.id == plugin.id }\n                        .joinToString { it.packageName }\n                    val message = \"Conflicting plugins found from: $packages\"\n                    Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()\n                    throw IllegalStateException(message)\n                }*/\n            }\n            check(put(plugin.id, plugin))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.annotation.SuppressLint\nimport android.content.BroadcastReceiver\nimport android.content.ContentResolver\nimport android.content.Intent\nimport android.content.pm.ComponentInfo\nimport android.content.pm.PackageManager\nimport android.content.pm.ProviderInfo\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Build\nimport android.system.Os\nimport android.widget.Toast\nimport androidx.core.os.bundleOf\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.listenForPackageChanges\nimport java.io.File\nimport java.io.FileNotFoundException\n\nobject PluginManager {\n\n    class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin),\n        BaseService.ExpectedException {\n        override fun getLocalizedMessage() =\n            SagerNet.application.getString(R.string.plugin_unknown, plugin)\n    }\n\n    private var receiver: BroadcastReceiver? = null\n    private var cachedPlugins: PluginList? = null\n    fun fetchPlugins() = synchronized(this) {\n        if (receiver == null) receiver = SagerNet.application.listenForPackageChanges {\n            synchronized(this) {\n                receiver = null\n                cachedPlugins = null\n            }\n        }\n        if (cachedPlugins == null) cachedPlugins = PluginList()\n        cachedPlugins!!\n    }\n\n    private fun buildUri(id: String) = Uri.Builder()\n        .scheme(PluginContract.SCHEME)\n        .authority(PluginContract.AUTHORITY)\n        .path(\"/$id\")\n        .build()\n\n    data class InitResult(\n        val path: String,\n    )\n\n    @Throws(Throwable::class)\n    fun init(pluginId: String): InitResult? {\n        if (pluginId.isEmpty()) return null\n        var throwable: Throwable? = null\n\n        try {\n            val result = initNative(pluginId)\n            if (result != null) return result\n        } catch (t: Throwable) {\n            if (throwable == null) throwable = t else Logs.w(t)\n        }\n\n        throw throwable ?: PluginNotFoundException(pluginId)\n    }\n\n    private fun initNative(pluginId: String): InitResult? {\n        var flags = PackageManager.GET_META_DATA\n        if (Build.VERSION.SDK_INT >= 24) {\n            flags =\n                flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE\n        }\n        val providers = SagerNet.application.packageManager.queryIntentContentProviders(\n            Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId)), flags)\n            .filter { it.providerInfo.exported }\n        if (providers.isEmpty()) return null\n        if (providers.size > 1) {\n            val message =\n                \"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}\"\n            Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()\n            throw IllegalStateException(message)\n        }\n        val provider = providers.single().providerInfo\n        var failure: Throwable? = null\n        try {\n            initNativeFaster(provider)?.also { return InitResult(it) }\n        } catch (t: Throwable) {\n            Logs.w(\"Initializing native plugin faster mode failed\")\n            failure = t\n        }\n\n        val uri = Uri.Builder().apply {\n            scheme(ContentResolver.SCHEME_CONTENT)\n            authority(provider.authority)\n        }.build()\n        try {\n            return initNativeFast(SagerNet.application.contentResolver,\n                pluginId,\n                uri)?.let { InitResult(it) }\n        } catch (t: Throwable) {\n            Logs.w(\"Initializing native plugin fast mode failed\")\n            failure?.also { t.addSuppressed(it) }\n            failure = t\n        }\n\n        try {\n            return initNativeSlow(SagerNet.application.contentResolver,\n                pluginId,\n                uri)?.let { InitResult(it) }\n        } catch (t: Throwable) {\n            failure?.also { t.addSuppressed(it) }\n            throw t\n        }\n    }\n\n    private fun initNativeFaster(provider: ProviderInfo): String? {\n        return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)\n            ?.let { relativePath ->\n                File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {\n                    check(canExecute())\n                }.absolutePath\n            }\n    }\n\n    private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {\n        return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())\n            ?.getString(PluginContract.EXTRA_ENTRY)?.also {\n                check(File(it).canExecute())\n            }\n    }\n\n    @SuppressLint(\"Recycle\")\n    private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {\n        var initialized = false\n        fun entryNotFound(): Nothing =\n            throw IndexOutOfBoundsException(\"Plugin entry binary not found\")\n\n        val pluginDir = File(SagerNet.deviceStorage.noBackupFilesDir, \"plugin\")\n        (cr.query(uri,\n            arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),\n            null,\n            null,\n            null)\n            ?: return null).use { cursor ->\n            if (!cursor.moveToFirst()) entryNotFound()\n            pluginDir.deleteRecursively()\n            if (!pluginDir.mkdirs()) throw FileNotFoundException(\"Unable to create plugin directory\")\n            val pluginDirPath = pluginDir.absolutePath + '/'\n            do {\n                val path = cursor.getString(0)\n                val file = File(pluginDir, path)\n                check(file.absolutePath.startsWith(pluginDirPath))\n                cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->\n                    file.outputStream().use { outStream -> inStream.copyTo(outStream) }\n                }\n                Os.chmod(file.absolutePath, when (cursor.getType(1)) {\n                    Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)\n                    Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)\n                    else -> throw IllegalArgumentException(\"File mode should be of type int\")\n                })\n                if (path == pluginId) initialized = true\n            } while (cursor.moveToNext())\n        }\n        if (!initialized) entryNotFound()\n        return File(pluginDir, pluginId).absolutePath\n    }\n\n    fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {\n        is String -> value\n        is Int -> SagerNet.application.packageManager.getResourcesForApplication(applicationInfo)\n            .getString(value)\n        null -> null\n        else -> error(\"meta-data $key has invalid type ${value.javaClass}\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/plugin/ResolvedPlugin.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.content.pm.ComponentInfo\nimport android.content.pm.ResolveInfo\nimport android.graphics.drawable.Drawable\nimport android.os.Build\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.plugin.PluginManager.loadString\n\nabstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {\n    protected abstract val componentInfo: ComponentInfo\n\n    override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }\n    override val version by lazy {\n        SagerNet.application.getPackageInfo(componentInfo.packageName).versionCode\n    }\n    override val versionName by lazy {\n        SagerNet.application.getPackageInfo(componentInfo.packageName).versionName\n    }\n    override val label: CharSequence get() = resolveInfo.loadLabel(SagerNet.application.packageManager)\n    override val icon: Drawable get() = resolveInfo.loadIcon(SagerNet.application.packageManager)\n    override val packageName: String get() = componentInfo.packageName\n    override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.PowerManager\nimport android.os.SystemClock\nimport android.provider.Settings\nimport android.text.util.Linkify\nimport android.view.View\nimport androidx.activity.result.component1\nimport androidx.activity.result.component2\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.view.ViewCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport com.danielstone.materialaboutlibrary.MaterialAboutFragment\nimport com.danielstone.materialaboutlibrary.items.MaterialAboutActionItem\nimport com.danielstone.materialaboutlibrary.model.MaterialAboutCard\nimport com.danielstone.materialaboutlibrary.model.MaterialAboutList\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.databinding.LayoutAboutBinding\nimport io.nekohasekai.sagernet.fmt.PluginEntry\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.plugin.PluginManager\nimport io.nekohasekai.sagernet.widget.ListHolderListener\nimport libcore.Libcore\n\nclass AboutFragment : ToolbarFragment(R.layout.layout_about) {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val binding = LayoutAboutBinding.bind(view)\n\n        ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)\n        toolbar.setTitle(R.string.menu_about)\n\n        var eTime = 0L\n        var eCount = 0\n\n        binding.titleCard.setOnClickListener {\n            val time = SystemClock.elapsedRealtime()\n            if (time - eTime >= 1000L) eCount = 1 else if (++eCount >= 3) {\n                requireContext().launchCustomTab(\"https://github.com/XTLS\")\n            }\n            eTime = time\n        }\n\n        parentFragmentManager.beginTransaction()\n            .replace(R.id.about_fragment_holder, AboutContent())\n            .commitAllowingStateLoss()\n\n        runOnDefaultDispatcher {\n            val license = view.context.assets.open(\"LICENSE\").bufferedReader().readText()\n            onMainDispatcher {\n                binding.license.text = license\n                Linkify.addLinks(binding.license, Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS)\n            }\n        }\n    }\n\n    class AboutContent : MaterialAboutFragment() {\n\n        val requestIgnoreBatteryOptimizations = registerForActivityResult(\n            ActivityResultContracts.StartActivityForResult()\n        ) { (resultCode, _) ->\n            if (resultCode == Activity.RESULT_OK) {\n                parentFragmentManager.beginTransaction()\n                    .replace(R.id.about_fragment_holder, AboutContent())\n                    .commitAllowingStateLoss()\n            }\n        }\n\n        override fun getMaterialAboutList(activityContext: Context): MaterialAboutList {\n\n            var versionName = BuildConfig.VERSION_NAME\n            if (isExpert) {\n                versionName += \"-${BuildConfig.FLAVOR}\"\n            }\n\n            return MaterialAboutList.Builder()\n                .addCard(MaterialAboutCard.Builder()\n                    .outline(false)\n                    .addItem(MaterialAboutActionItem.Builder()\n                        .icon(R.drawable.ic_baseline_update_24)\n                        .text(R.string.app_version)\n                        .subText(versionName)\n                        .setOnClickAction {\n                            requireContext().launchCustomTab(\n                                \"https://github.com/XTLS/AnXray/releases\"\n                            )\n                        }\n                        .build())\n                    .addItem(MaterialAboutActionItem.Builder()\n                        .icon(R.drawable.ic_baseline_airplanemode_active_24)\n                        .text(getString(R.string.version_x, \"Xray-core\"))\n                        .subText(\"v\" + Libcore.getV2RayVersion())\n                        .setOnClickAction {\n                            requireContext().launchCustomTab(\n                                \"https://github.com/XTLS/Xray-core/releases\"\n                            )\n                        }\n                        .build())\n                    .apply {\n                        val m = enumValues<PluginEntry>().associateBy { it.pluginId }\n                        for (plugin in PluginManager.fetchPlugins()) {\n                            if (!m.containsKey(plugin.id)) continue\n                            try {\n                                addItem(MaterialAboutActionItem.Builder()\n                                    .icon(R.drawable.ic_baseline_nfc_24)\n                                    .text(getString(R.string.version_x, plugin.id))\n                                    .subText(\"v\" + plugin.versionName)\n                                    .setOnClickAction {\n                                        startActivity(Intent().apply {\n                                            action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS\n                                            data = Uri.fromParts(\n                                                \"package\", plugin.packageName, null\n                                            )\n                                        })\n                                    }\n                                    .build())\n                            } catch (e: Exception) {\n                                Logs.w(e)\n                            }\n                        }\n                    }\n                    .apply {\n                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                            val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager\n                            if (!pm.isIgnoringBatteryOptimizations(app.packageName)) {\n                                addItem(MaterialAboutActionItem.Builder()\n                                    .icon(R.drawable.ic_baseline_running_with_errors_24)\n                                    .text(R.string.ignore_battery_optimizations)\n                                    .subText(R.string.ignore_battery_optimizations_sum)\n                                    .setOnClickAction {\n                                        requestIgnoreBatteryOptimizations.launch(\n                                            Intent(\n                                                Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,\n                                                Uri.parse(\"package:${app.packageName}\")\n                                            )\n                                        )\n                                    }\n                                    .build())\n                            }\n                        }\n                        addItem(MaterialAboutActionItem.Builder()\n                            .icon(R.drawable.ic_baseline_card_giftcard_24)\n                            .text(R.string.donate)\n                            .subText(R.string.donate_info)\n                            .setOnClickAction {\n                                requireContext().launchCustomTab(\n                                    \"https://liberapay.com/nekohasekai\"\n                                )\n                            }\n                            .build())\n                    }\n                    .build())\n                .addCard(MaterialAboutCard.Builder()\n                    .outline(false)\n                    .title(R.string.project)\n                    .addItem(MaterialAboutActionItem.Builder()\n                        .icon(R.drawable.ic_baseline_sanitizer_24)\n                        .text(R.string.github)\n                        .setOnClickAction {\n                            requireContext().launchCustomTab(\n                                \"https://github.com/XTLS/AnXray\"\n\n                            )\n                        }\n                        .build())\n                    .addItem(MaterialAboutActionItem.Builder()\n                        .icon(R.drawable.baseline_translate_24)\n                        .text(R.string.translate_platform)\n                        .setOnClickAction {\n                            requireContext().launchCustomTab(\n                                \"https://hosted.weblate.org/engage/sagernet/\"\n\n                            )\n                        }\n                        .build())\n                    .addItem(MaterialAboutActionItem.Builder()\n                        .icon(R.drawable.ic_qu_shadowsocks_foreground)\n                        .text(R.string.telegram)\n                        .setOnClickAction {\n                            requireContext().launchCustomTab(\n                                \"https://t.me/AnXray\"\n                            )\n                        }\n                        .build())\n                    .addItem(MaterialAboutActionItem.Builder()\n                        .icon(R.drawable.ic_action_copyright)\n                        .text(R.string.oss_licenses)\n                        .setOnClickAction {\n                            startActivity(Intent(context, LicenseActivity::class.java))\n                        }\n                        .build())\n                    .build())\n                .build()\n\n        }\n\n        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n            super.onViewCreated(view, savedInstanceState)\n\n            view.findViewById<RecyclerView>(R.id.mal_recyclerview).apply {\n                overScrollMode = RecyclerView.OVER_SCROLL_NEVER\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ActiveFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.text.format.Formatter\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.RecyclerView\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.aidl.AppStats\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.databinding.LayoutTrafficItemBinding\nimport io.nekohasekai.sagernet.databinding.LayoutTrafficListBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.utils.PackageCache\n\nclass ActiveFragment : Fragment(R.layout.layout_traffic_list) {\n\n    lateinit var binding: LayoutTrafficListBinding\n    lateinit var adapter: ActiveAdapter\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        binding = LayoutTrafficListBinding.bind(view)\n        adapter = ActiveAdapter()\n        binding.trafficList.layoutManager = FixedLinearLayoutManager(binding.trafficList)\n        binding.trafficList.adapter = adapter\n        (parentFragment as TrafficFragment).listeners.add(::emitStats)\n        emitStats(emptyList())\n    }\n\n    fun emitStats(statsList: List<AppStats>) {\n        if (statsList.isEmpty()) {\n            runOnMainDispatcher {\n                binding.holder.isVisible = true\n                binding.trafficList.isVisible = false\n\n                if (!SagerNet.started || DataStore.serviceMode != Key.MODE_VPN) {\n                    binding.holder.text = getString(R.string.traffic_holder)\n                } else if ((activity as MainActivity).connection.service?.trafficStatsEnabled != true) {\n                    binding.holder.text = getString(R.string.app_statistics_disabled)\n                } else {\n                    binding.holder.text = getString(R.string.no_statistics)\n                }\n            }\n            binding.trafficList.post {\n                adapter.data = emptyList()\n                adapter.notifyDataSetChanged()\n            }\n        } else {\n            runOnMainDispatcher {\n                binding.holder.isVisible = false\n                binding.trafficList.isVisible = true\n            }\n\n            val now = System.currentTimeMillis() / 1000\n            val list = statsList.filter { it.deactivateAt == 0 || now - it.deactivateAt < 5 }\n                .toSortedSet { a, b ->\n                    val dataA = a.uplink + a.downlink\n                    val dataB = b.uplink + b.downlink\n                    if (dataA != dataB) {\n                        dataB.compareTo(dataA)\n                    } else {\n                        val connA = a.tcpConnections + a.udpConnections\n                        val connB = b.tcpConnections + b.udpConnections\n                        if (connA != connB) {\n                            connB.compareTo(connA)\n                        } else {\n                            b.packageName.compareTo(a.packageName)\n                        }\n                    }\n                }\n                .toList()\n            binding.trafficList.post {\n                adapter.data = list\n                adapter.notifyDataSetChanged()\n            }\n        }\n    }\n\n    inner class ActiveAdapter : RecyclerView.Adapter<ActiveViewHolder>() {\n\n        init {\n            setHasStableIds(true)\n        }\n\n        lateinit var data: List<AppStats>\n\n        override fun getItemId(position: Int): Long {\n            return data[position].uid.toLong()\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActiveViewHolder {\n            return ActiveViewHolder(\n                LayoutTrafficItemBinding.inflate(layoutInflater, parent, false)\n            )\n        }\n\n        override fun onBindViewHolder(holder: ActiveViewHolder, position: Int) {\n            holder.bind(data[position])\n        }\n\n        override fun getItemCount(): Int {\n            if (!::data.isInitialized) return 0\n            return data.size\n        }\n    }\n\n    inner class ActiveViewHolder(val binding: LayoutTrafficItemBinding) : RecyclerView.ViewHolder(\n        binding.root\n    ) {\n\n        fun bind(stats: AppStats) {\n            PackageCache.awaitLoadSync()\n\n            val packageName = if (stats.uid > 1000) {\n                PackageCache.uidMap[stats.uid]?.iterator()?.next() ?: \"android\"\n            } else {\n                \"android\"\n            }\n\n            binding.menu.setOnClickListener {\n                val popup = PopupMenu(requireContext(), it)\n                popup.menuInflater.inflate(R.menu.traffic_item_menu, popup.menu)\n                popup.setOnMenuItemClickListener(\n                    (requireParentFragment() as TrafficFragment).ItemMenuListener(\n                        stats\n                    )\n                )\n                popup.show()\n            }\n\n            binding.label.text = PackageCache.loadLabel(packageName)\n            binding.desc.text = \"$packageName (${stats.uid})\"\n            binding.tcpConnections.text = getString(R.string.tcp_connections, stats.tcpConnections)\n            binding.udpConnections.text = getString(R.string.udp_connections, stats.udpConnections)\n            binding.trafficUplink.text = getString(\n                R.string.traffic_uplink,\n                Formatter.formatFileSize(requireContext(), stats.uplinkTotal),\n                Formatter.formatFileSize(requireContext(), stats.uplink)\n            )\n            binding.trafficDownlink.text = getString(\n                R.string.traffic_downlink,\n                Formatter.formatFileSize(requireContext(), stats.downlinkTotal),\n                Formatter.formatFileSize(requireContext(), stats.downlink)\n            )\n            val info = PackageCache.installedApps[packageName]\n            if (info != null) runOnDefaultDispatcher {\n                try {\n                    val icon = info.loadIcon(app.packageManager)\n                    onMainDispatcher {\n                        binding.icon.setImageDrawable(icon)\n                    }\n                } catch (ignored: Exception) {\n                }\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageManager\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\nimport android.util.SparseBooleanArray\nimport android.view.*\nimport android.widget.Filter\nimport android.widget.Filterable\nimport androidx.annotation.UiThread\nimport androidx.core.util.contains\nimport androidx.core.util.set\nimport androidx.core.view.ViewCompat\nimport androidx.core.widget.addTextChangedListener\nimport androidx.lifecycle.lifecycleScope\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.snackbar.Snackbar\nimport com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.databinding.LayoutAppListBinding\nimport io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding\nimport io.nekohasekai.sagernet.ktx.crossFadeFrom\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport io.nekohasekai.sagernet.widget.ListHolderListener\nimport io.nekohasekai.sagernet.widget.ListListener\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.withContext\nimport kotlin.coroutines.coroutineContext\n\nclass AppListActivity : ThemedActivity() {\n    companion object {\n        private const val SWITCH = \"switch\"\n\n        private val cachedApps\n            get() = PackageCache.installedPackages.toMutableMap().apply {\n                remove(BuildConfig.APPLICATION_ID)\n            }\n    }\n\n    private class ProxiedApp(\n        private val pm: PackageManager, private val appInfo: ApplicationInfo,\n        val packageName: String,\n    ) {\n        val name: CharSequence = appInfo.loadLabel(pm)    // cached for sorting\n        val icon: Drawable get() = appInfo.loadIcon(pm)\n        val uid get() = appInfo.uid\n        val sys get() = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0\n    }\n\n    private inner class AppViewHolder(val binding: LayoutAppsItemBinding) : RecyclerView.ViewHolder(\n        binding.root\n    ),\n        View.OnClickListener {\n        private lateinit var item: ProxiedApp\n\n        init {\n            binding.root.setOnClickListener(this)\n        }\n\n        fun bind(app: ProxiedApp) {\n            item = app\n            binding.itemicon.setImageDrawable(app.icon)\n            binding.title.text = app.name\n            binding.desc.text = \"${app.packageName} (${app.uid})\"\n            binding.itemcheck.isChecked = isProxiedApp(app)\n        }\n\n        fun handlePayload(payloads: List<String>) {\n            if (payloads.contains(SWITCH)) binding.itemcheck.isChecked = isProxiedApp(item)\n        }\n\n        override fun onClick(v: View?) {\n            if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true\n            DataStore.routePackages = apps.filter { isProxiedApp(it) }\n                .joinToString(\"\\n\") { it.packageName }\n\n            appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)\n        }\n    }\n\n    private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(),\n        Filterable,\n        FastScrollRecyclerView.SectionedAdapter {\n        var filteredApps = apps\n\n        suspend fun reload() {\n            apps = cachedApps.map { (packageName, packageInfo) ->\n                coroutineContext[Job]!!.ensureActive()\n                ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)\n            }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n        }\n\n        override fun onBindViewHolder(holder: AppViewHolder, position: Int) =\n            holder.bind(filteredApps[position])\n\n        override fun onBindViewHolder(holder: AppViewHolder, position: Int, payloads: List<Any>) {\n            if (payloads.isNotEmpty()) {\n                @Suppress(\"UNCHECKED_CAST\") holder.handlePayload(payloads as List<String>)\n                return\n            }\n\n            onBindViewHolder(holder, position)\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder =\n            AppViewHolder(LayoutAppsItemBinding.inflate(layoutInflater, parent, false))\n\n        override fun getItemCount(): Int = filteredApps.size\n\n        private val filterImpl = object : Filter() {\n            override fun performFiltering(constraint: CharSequence) = FilterResults().apply {\n                var filteredApps = if (constraint.isEmpty()) apps else apps.filter {\n                    it.name.contains(constraint, true) || it.packageName.contains(\n                        constraint, true\n                    ) || it.uid.toString().contains(constraint)\n                }\n                if (!sysApps) filteredApps = filteredApps.filter { !it.sys }\n                count = filteredApps.size\n                values = filteredApps\n            }\n\n            override fun publishResults(constraint: CharSequence, results: FilterResults) {\n                @Suppress(\"UNCHECKED_CAST\") filteredApps = results.values as List<ProxiedApp>\n                notifyDataSetChanged()\n            }\n        }\n\n        override fun getFilter(): Filter = filterImpl\n\n        override fun getSectionName(position: Int): String {\n            return filteredApps[position].name.firstOrNull()?.toString() ?: \"\"\n        }\n\n    }\n\n    private val loading by lazy { findViewById<View>(R.id.loading) }\n\n    private lateinit var binding: LayoutAppListBinding\n    private val proxiedUids = SparseBooleanArray()\n    private var loader: Job? = null\n    private var apps = emptyList<ProxiedApp>()\n    private val appsAdapter = AppsAdapter()\n\n    private fun initProxiedUids(str: String = DataStore.routePackages) {\n        proxiedUids.clear()\n        val apps = cachedApps\n        for (line in str.lineSequence()) proxiedUids[(apps[line]\n            ?: continue).applicationInfo.uid] = true\n    }\n\n    private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid]\n\n    @UiThread\n    private fun loadApps() {\n        loader?.cancel()\n        loader = lifecycleScope.launchWhenCreated {\n            loading.crossFadeFrom(binding.list)\n            val adapter = binding.list.adapter as AppsAdapter\n            withContext(Dispatchers.IO) { adapter.reload() }\n            adapter.filter.filter(binding.search.text?.toString() ?: \"\")\n            binding.list.crossFadeFrom(loading)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        binding = LayoutAppListBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        ListHolderListener.setup(this)\n        setSupportActionBar(binding.toolbar)\n        supportActionBar?.apply {\n            setTitle(R.string.select_apps)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        initProxiedUids()\n        binding.list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)\n        binding.list.itemAnimator = DefaultItemAnimator()\n        binding.list.adapter = appsAdapter\n\n        ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener)\n\n        binding.search.addTextChangedListener {\n            appsAdapter.filter.filter(it?.toString() ?: \"\")\n        }\n\n        binding.showSystemApps.isChecked = sysApps\n        binding.showSystemApps.setOnCheckedChangeListener { _, isChecked ->\n            sysApps = isChecked\n            appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n        }\n\n        loadApps()\n    }\n\n    private var sysApps = false\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.app_list_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_invert_selections -> {\n                runOnDefaultDispatcher {\n                    for (app in apps) {\n                        if (proxiedUids.contains(app.uid)) {\n                            proxiedUids.delete(app.uid)\n                        } else {\n                            proxiedUids[app.uid] = true\n                        }\n                    }\n                    DataStore.routePackages = apps.filter { isProxiedApp(it) }\n                        .joinToString(\"\\n\") { it.packageName }\n                    apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n                    onMainDispatcher {\n                        appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n                    }\n                }\n\n                return true\n            }\n            R.id.action_clear_selections -> {\n                runOnDefaultDispatcher {\n                    proxiedUids.clear()\n                    DataStore.routePackages = \"\"\n                    apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n                    onMainDispatcher {\n                        appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n                    }\n                }\n            }\n            R.id.action_export_clipboard -> {\n                val success = SagerNet.trySetPrimaryClip(\"false\\n${DataStore.routePackages}\")\n                Snackbar.make(\n                    binding.list,\n                    if (success) R.string.action_export_msg else R.string.action_export_err,\n                    Snackbar.LENGTH_LONG\n                ).show()\n                return true\n            }\n            R.id.action_import_clipboard -> {\n                val proxiedAppString = SagerNet.clipboard.primaryClip?.getItemAt(0)?.text?.toString()\n                if (!proxiedAppString.isNullOrEmpty()) {\n                    val i = proxiedAppString.indexOf('\\n')\n                    try {\n                        val apps = if (i < 0) \"\" else proxiedAppString.substring(i + 1)\n                        DataStore.routePackages = apps\n                        Snackbar.make(\n                            binding.list, R.string.action_import_msg, Snackbar.LENGTH_LONG\n                        ).show()\n                        initProxiedUids(apps)\n                        appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)\n                        return true\n                    } catch (_: IllegalArgumentException) {\n                    }\n                }\n                Snackbar.make(binding.list, R.string.action_import_err, Snackbar.LENGTH_LONG).show()\n            }\n        }\n        return super.onOptionsItemSelected(item)\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        if (!super.onSupportNavigateUp()) finish()\n        return true\n    }\n\n    override fun supportNavigateUpTo(upIntent: Intent) =\n        super.supportNavigateUpTo(upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))\n\n    override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU) {\n        if (binding.toolbar.isOverflowMenuShowing) binding.toolbar.hideOverflowMenu() else binding.toolbar.showOverflowMenu()\n    } else super.onKeyUp(keyCode, event)\n\n    override fun onDestroy() {\n        loader?.cancel()\n        super.onDestroy()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageManager\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\nimport android.util.SparseBooleanArray\nimport android.view.*\nimport android.widget.Filter\nimport android.widget.Filterable\nimport android.widget.TextView\nimport androidx.annotation.UiThread\nimport androidx.core.util.contains\nimport androidx.core.util.set\nimport androidx.core.view.ViewCompat\nimport androidx.core.widget.addTextChangedListener\nimport androidx.lifecycle.lifecycleScope\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.snackbar.Snackbar\nimport com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.databinding.LayoutAppsBinding\nimport io.nekohasekai.sagernet.databinding.LayoutAppsItemBinding\nimport io.nekohasekai.sagernet.databinding.LayoutLoadingBinding\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.crossFadeFrom\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport io.nekohasekai.sagernet.widget.ListHolderListener\nimport io.nekohasekai.sagernet.widget.ListListener\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.withContext\nimport okhttp3.internal.closeQuietly\nimport org.jf.dexlib2.dexbacked.DexBackedDexFile\nimport org.jf.dexlib2.iface.DexFile\nimport java.io.File\nimport java.util.zip.ZipException\nimport java.util.zip.ZipFile\nimport kotlin.coroutines.coroutineContext\n\nclass AppManagerActivity : ThemedActivity() {\n    companion object {\n        @SuppressLint(\"StaticFieldLeak\")\n        private var instance: AppManagerActivity? = null\n        private const val SWITCH = \"switch\"\n\n        private val cachedApps\n            get() = PackageCache.installedPackages.toMutableMap().apply {\n                remove(BuildConfig.APPLICATION_ID)\n            }\n    }\n\n    private class ProxiedApp(\n        private val pm: PackageManager, private val appInfo: ApplicationInfo,\n        val packageName: String,\n    ) {\n        val name: CharSequence = appInfo.loadLabel(pm)    // cached for sorting\n        val icon: Drawable get() = appInfo.loadIcon(pm)\n        val uid get() = appInfo.uid\n        val sys get() = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0\n    }\n\n    private inner class AppViewHolder(val binding: LayoutAppsItemBinding) : RecyclerView.ViewHolder(\n        binding.root\n    ),\n        View.OnClickListener {\n        private lateinit var item: ProxiedApp\n\n        init {\n            binding.root.setOnClickListener(this)\n        }\n\n        fun bind(app: ProxiedApp) {\n            item = app\n            binding.itemicon.setImageDrawable(app.icon)\n            binding.title.text = app.name\n            binding.desc.text = \"${app.packageName} (${app.uid})\"\n            binding.itemcheck.isChecked = isProxiedApp(app)\n        }\n\n        fun handlePayload(payloads: List<String>) {\n            if (payloads.contains(SWITCH)) binding.itemcheck.isChecked = isProxiedApp(item)\n        }\n\n        override fun onClick(v: View?) {\n            if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true\n            DataStore.individual = apps.filter { isProxiedApp(it) }\n                .joinToString(\"\\n\") { it.packageName }\n\n            appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)\n        }\n    }\n\n    private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(),\n        Filterable,\n        FastScrollRecyclerView.SectionedAdapter {\n        var filteredApps = apps\n\n        suspend fun reload() {\n            apps = cachedApps.map { (packageName, packageInfo) ->\n                coroutineContext[Job]!!.ensureActive()\n                ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)\n            }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n        }\n\n        override fun onBindViewHolder(holder: AppViewHolder, position: Int) =\n            holder.bind(filteredApps[position])\n\n        override fun onBindViewHolder(holder: AppViewHolder, position: Int, payloads: List<Any>) {\n            if (payloads.isNotEmpty()) {\n                @Suppress(\"UNCHECKED_CAST\") holder.handlePayload(payloads as List<String>)\n                return\n            }\n\n            onBindViewHolder(holder, position)\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder =\n            AppViewHolder(LayoutAppsItemBinding.inflate(layoutInflater, parent, false))\n\n        override fun getItemCount(): Int = filteredApps.size\n\n        private val filterImpl = object : Filter() {\n            override fun performFiltering(constraint: CharSequence) = FilterResults().apply {\n                var filteredApps = if (constraint.isEmpty()) apps else apps.filter {\n                    it.name.contains(constraint, true) || it.packageName.contains(\n                        constraint, true\n                    ) || it.uid.toString().contains(constraint)\n                }\n                if (!sysApps) filteredApps = filteredApps.filter { !it.sys }\n                count = filteredApps.size\n                values = filteredApps\n            }\n\n            override fun publishResults(constraint: CharSequence, results: FilterResults) {\n                @Suppress(\"UNCHECKED_CAST\") filteredApps = results.values as List<ProxiedApp>\n                notifyDataSetChanged()\n            }\n        }\n\n        override fun getFilter(): Filter = filterImpl\n\n        override fun getSectionName(position: Int): String {\n            return filteredApps[position].name.firstOrNull()?.toString() ?: \"\"\n        }\n\n    }\n\n    private val loading by lazy { findViewById<View>(R.id.loading) }\n\n    private lateinit var binding: LayoutAppsBinding\n    private val proxiedUids = SparseBooleanArray()\n    private var loader: Job? = null\n    private var apps = emptyList<ProxiedApp>()\n    private val appsAdapter = AppsAdapter()\n\n    private fun initProxiedUids(str: String = DataStore.individual) {\n        proxiedUids.clear()\n        val apps = cachedApps\n        for (line in str.lineSequence()) proxiedUids[(apps[line]\n            ?: continue).applicationInfo.uid] = true\n    }\n\n    private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid]\n\n    @UiThread\n    private fun loadApps() {\n        loader?.cancel()\n        loader = lifecycleScope.launchWhenCreated {\n            loading.crossFadeFrom(binding.list)\n            val adapter = binding.list.adapter as AppsAdapter\n            withContext(Dispatchers.IO) { adapter.reload() }\n            adapter.filter.filter(binding.search.text?.toString() ?: \"\")\n            binding.list.crossFadeFrom(loading)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        binding = LayoutAppsBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        ListHolderListener.setup(this)\n        setSupportActionBar(binding.toolbar)\n        supportActionBar?.apply {\n            setTitle(R.string.proxied_apps)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        if (!DataStore.proxyApps) {\n            DataStore.proxyApps = true\n        }\n\n        binding.bypassGroup.check(if (DataStore.bypass) R.id.appProxyModeBypass else R.id.appProxyModeOn)\n        binding.bypassGroup.setOnCheckedChangeListener { _, checkedId ->\n            when (checkedId) {\n                R.id.appProxyModeDisable -> {\n                    DataStore.proxyApps = false\n                    finish()\n                }\n                R.id.appProxyModeOn -> DataStore.bypass = false\n                R.id.appProxyModeBypass -> DataStore.bypass = true\n            }\n        }\n\n        initProxiedUids()\n        binding.list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)\n        binding.list.itemAnimator = DefaultItemAnimator()\n        binding.list.adapter = appsAdapter\n\n        ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener)\n\n        binding.search.addTextChangedListener {\n            appsAdapter.filter.filter(it?.toString() ?: \"\")\n        }\n\n        binding.showSystemApps.isChecked = sysApps\n        binding.showSystemApps.setOnCheckedChangeListener { _, isChecked ->\n            sysApps = isChecked\n            appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n        }\n\n        instance = this\n        loadApps()\n    }\n\n    private var sysApps = true\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.per_app_proxy_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_scan_china_apps -> {\n                scanChinaApps()\n                return true\n            }\n            R.id.action_invert_selections -> {\n                runOnDefaultDispatcher {\n                    for (app in apps) {\n                        if (proxiedUids.contains(app.uid)) {\n                            proxiedUids.delete(app.uid)\n                        } else {\n                            proxiedUids[app.uid] = true\n                        }\n                    }\n                    DataStore.individual = apps.filter { isProxiedApp(it) }\n                        .joinToString(\"\\n\") { it.packageName }\n                    apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n                    onMainDispatcher {\n                        appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n                    }\n                }\n\n                return true\n            }\n            R.id.action_clear_selections -> {\n                runOnDefaultDispatcher {\n                    proxiedUids.clear()\n                    DataStore.individual = \"\"\n                    apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n                    onMainDispatcher {\n                        appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n                    }\n                }\n            }\n            R.id.action_export_clipboard -> {\n                val success = SagerNet.trySetPrimaryClip(\"${DataStore.bypass}\\n${DataStore.individual}\")\n                Snackbar.make(\n                    binding.list,\n                    if (success) R.string.action_export_msg else R.string.action_export_err,\n                    Snackbar.LENGTH_LONG\n                ).show()\n                return true\n            }\n            R.id.action_import_clipboard -> {\n                val proxiedAppString = SagerNet.clipboard.primaryClip?.getItemAt(0)?.text?.toString()\n                if (!proxiedAppString.isNullOrEmpty()) {\n                    val i = proxiedAppString.indexOf('\\n')\n                    try {\n                        val (enabled, apps) = if (i < 0) {\n                            proxiedAppString to \"\"\n                        } else proxiedAppString.substring(\n                            0, i\n                        ) to proxiedAppString.substring(i + 1)\n                        binding.bypassGroup.check(if (enabled.toBoolean()) R.id.appProxyModeBypass else R.id.appProxyModeOn)\n                        DataStore.individual = apps\n                        Snackbar.make(\n                            binding.list, R.string.action_import_msg, Snackbar.LENGTH_LONG\n                        ).show()\n                        initProxiedUids(apps)\n                        appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)\n                        return true\n                    } catch (_: IllegalArgumentException) {\n                    }\n                }\n                Snackbar.make(binding.list, R.string.action_import_err, Snackbar.LENGTH_LONG).show()\n            }\n        }\n        return super.onOptionsItemSelected(item)\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun scanChinaApps() {\n\n        val text: TextView\n\n        val dialog = MaterialAlertDialogBuilder(this).setView(\n            LayoutLoadingBinding.inflate(layoutInflater).apply {\n                text = loadingText\n            }.root\n        ).setCancelable(false).show()\n\n        val txt = text.text.toString()\n\n        runOnDefaultDispatcher {\n            val chinaApps = ArrayList<Pair<PackageInfo, String>>()\n            val chinaRegex = (\"(\" + arrayOf(\n                \"com.tencent\",\n                \"com.alibaba\",\n                \"com.umeng\",\n                \"com.qihoo\",\n                \"com.ali\",\n                \"com.alipay\",\n                \"com.amap\",\n                \"com.sina\",\n                \"com.weibo\",\n                \"com.vivo\",\n                \"com.xiaomi\",\n                \"com.huawei\",\n                \"com.taobao\",\n                \"com.secneo\",\n                \"s.h.e.l.l\",\n                \"com.stub\",\n                \"com.kiwisec\",\n                \"com.secshell\",\n                \"com.wrapper\",\n                \"cn.securitystack\",\n                \"com.mogosec\",\n                \"com.secoen\",\n                \"com.netease\",\n                \"com.mx\",\n                \"com.qq.e\",\n                \"com.baidu\",\n                \"com.bytedance\",\n                \"com.bugly\",\n                \"com.miui\",\n                \"com.oppo\",\n                \"com.coloros\",\n                \"com.iqoo\",\n                \"com.meizu\",\n                \"com.gionee\",\n                \"cn.nubia\"\n            ).joinToString(\"|\") { \"${it.replace(\".\", \"\\\\.\")}\\\\.\" } + \").*\").toRegex()\n\n            val bypass = DataStore.bypass\n            val cachedApps = cachedApps\n\n            apps = cachedApps.map { (packageName, packageInfo) ->\n                kotlin.coroutines.coroutineContext[Job]!!.ensureActive()\n                ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)\n            }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n\n            scan@ for ((pkg, app) in cachedApps.entries) {\n                /*if (!sysApps && app.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {\n                    continue\n                }*/\n\n                val index = appsAdapter.filteredApps.indexOfFirst { it.uid == app.applicationInfo.uid }\n                var changed = false\n\n                onMainDispatcher {\n                    text.text = (txt + \" \" + app.packageName + \"\\n\\n\" + chinaApps.map { it.second }\n                        .reversed()\n                        .joinToString(\"\\n\", postfix = \"\\n\")).trim()\n                }\n\n                try {\n\n                    val dex = File(app.applicationInfo.publicSourceDir)\n                    val zipFile = ZipFile(dex)\n                    var dexFile: DexFile\n\n                    for (entry in zipFile.entries()) {\n                        if (entry.name.startsWith(\"classes\") && entry.name.endsWith(\".dex\")) {\n                            val input = zipFile.getInputStream(entry).readBytes()\n                            dexFile = try {\n                                DexBackedDexFile.fromInputStream(null, input.inputStream())\n                            } catch (e: Exception) {\n                                Logs.w(e)\n                                break\n                            }\n                            for (clazz in dexFile.classes) {\n                                val clazzName = clazz.type.substring(1, clazz.type.length - 1)\n                                    .replace(\"/\", \".\")\n                                    .replace(\"$\", \".\")\n\n                                if (clazzName.matches(chinaRegex)) {\n                                    chinaApps.add(\n                                        app to app.applicationInfo.loadLabel(packageManager)\n                                            .toString()\n                                    )\n                                    zipFile.closeQuietly()\n\n                                    if (bypass) {\n                                        changed = !proxiedUids[app.applicationInfo.uid]\n                                        proxiedUids[app.applicationInfo.uid] = true\n                                    } else {\n                                        proxiedUids.delete(app.applicationInfo.uid)\n                                    }\n\n                                    continue@scan\n                                }\n                            }\n                        }\n                    }\n                    zipFile.closeQuietly()\n\n                    if (bypass) {\n                        proxiedUids.delete(app.applicationInfo.uid)\n                    } else {\n                        changed = !proxiedUids[index]\n                        proxiedUids[app.applicationInfo.uid] = true\n                    }\n\n                } catch (e: ZipException) {\n                    Logs.w(\"Error in pkg ${app.packageName}:${app.versionName}\", e)\n                    continue\n                }\n\n            }\n\n            DataStore.individual = apps.filter { isProxiedApp(it) }\n                .joinToString(\"\\n\") { it.packageName }\n\n            apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))\n\n            onMainDispatcher {\n                appsAdapter.filter.filter(binding.search.text?.toString() ?: \"\")\n\n                dialog.dismiss()\n            }\n\n        }\n\n\n    }\n\n    override fun supportNavigateUpTo(upIntent: Intent) =\n        super.supportNavigateUpTo(upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))\n\n    override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU) {\n        if (binding.toolbar.isOverflowMenuShowing) binding.toolbar.hideOverflowMenu() else binding.toolbar.showOverflowMenu()\n    } else super.onKeyUp(keyCode, event)\n\n    override fun onDestroy() {\n        instance = null\n        loader?.cancel()\n        super.onDestroy()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.provider.OpenableColumns\nimport android.text.format.DateFormat\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.ViewGroup\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.view.isInvisible\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\nimport cn.hutool.json.JSONObject\nimport com.google.android.material.snackbar.Snackbar\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.databinding.LayoutAssetItemBinding\nimport io.nekohasekai.sagernet.databinding.LayoutAssetsBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.widget.UndoSnackbarManager\nimport libcore.Libcore\nimport okhttp3.Request\nimport java.io.File\nimport java.io.FileNotFoundException\nimport java.util.*\nimport java.util.concurrent.atomic.AtomicInteger\n\nclass AssetsActivity : ThemedActivity() {\n\n    lateinit var adapter: AssetAdapter\n    lateinit var layout: LayoutAssetsBinding\n    lateinit var undoManager: UndoSnackbarManager<File>\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val binding = LayoutAssetsBinding.inflate(layoutInflater)\n        layout = binding\n        setContentView(binding.root)\n\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setTitle(R.string.route_assets)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        binding.recyclerView.layoutManager = FixedLinearLayoutManager(binding.recyclerView)\n        adapter = AssetAdapter()\n        binding.recyclerView.adapter = adapter\n\n        binding.refreshLayout.setOnRefreshListener {\n            adapter.reloadAssets()\n            binding.refreshLayout.isRefreshing = false\n        }\n        binding.refreshLayout.setColorSchemeColors(getColorAttr(R.attr.primaryOrTextPrimary))\n\n        undoManager = UndoSnackbarManager(this, adapter)\n\n        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(\n            0, ItemTouchHelper.START\n        ) {\n\n            override fun getSwipeDirs(\n                recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder\n            ): Int {\n                val index = viewHolder.bindingAdapterPosition\n                if (index < 2) return 0\n                return super.getSwipeDirs(recyclerView, viewHolder)\n            }\n\n            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n                val index = viewHolder.bindingAdapterPosition\n                adapter.remove(index)\n                undoManager.remove(index to (viewHolder as AssetHolder).file)\n            }\n\n            override fun onMove(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n                target: RecyclerView.ViewHolder\n            ) = false\n\n        }).attachToRecyclerView(binding.recyclerView)\n    }\n\n    override fun snackbarInternal(text: CharSequence): Snackbar {\n        return Snackbar.make(layout.coordinator, text, Snackbar.LENGTH_LONG)\n    }\n\n    val internalFiles = arrayOf(\"geoip.dat\", \"geosite.dat\")\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.import_asset_menu, menu)\n        return true\n    }\n\n    val importFile = registerForActivityResult(ActivityResultContracts.GetContent()) { file ->\n        if (file != null) {\n            val fileName = contentResolver.query(file, null, null, null, null)?.use { cursor ->\n                cursor.moveToFirst()\n                cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString)\n            }?.takeIf { it.isNotBlank() } ?: file.pathSegments.last()\n                .substringAfterLast('/')\n                .substringAfter(':')\n\n            if (!fileName.endsWith(\".dat\")) {\n                alert(getString(R.string.route_not_asset, fileName)).show()\n                return@registerForActivityResult\n            }\n            val filesDir = getExternalFilesDir(null) ?: filesDir\n\n            runOnDefaultDispatcher {\n                val outFile = File(filesDir, fileName).apply {\n                    parentFile?.mkdirs()\n                }\n\n                contentResolver.openInputStream(file)?.use(outFile.outputStream())\n\n                File(outFile.parentFile, outFile.nameWithoutExtension + \".version.txt\").apply {\n                    if (isFile) delete()\n                }\n\n                adapter.reloadAssets()\n            }\n\n        }\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_import_file -> {\n                startFilesForResult(importFile, \"*/*\")\n                return true\n            }\n        }\n        return false\n    }\n\n    inner class AssetAdapter : RecyclerView.Adapter<AssetHolder>(),\n        UndoSnackbarManager.Interface<File> {\n\n        val assets = ArrayList<File>()\n\n        init {\n            reloadAssets()\n        }\n\n        fun reloadAssets() {\n            val filesDir = getExternalFilesDir(null) ?: filesDir\n            val files = filesDir.listFiles()\n                ?.filter { it.isFile && it.name.endsWith(\".dat\") && it.name !in internalFiles }\n            assets.clear()\n            assets.add(File(filesDir, \"geoip.dat\"))\n            assets.add(\n                File(\n                    filesDir, \"geosite.dat\"\n                )\n            )\n            if (files != null) assets.addAll(files)\n\n            layout.refreshLayout.post {\n                notifyDataSetChanged()\n            }\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AssetHolder {\n            return AssetHolder(LayoutAssetItemBinding.inflate(layoutInflater, parent, false))\n        }\n\n        override fun onBindViewHolder(holder: AssetHolder, position: Int) {\n            holder.bind(assets[position])\n        }\n\n        override fun getItemCount(): Int {\n            return assets.size\n        }\n\n        fun remove(index: Int) {\n            assets.removeAt(index)\n            notifyItemRemoved(index)\n        }\n\n        override fun undo(actions: List<Pair<Int, File>>) {\n            for ((index, item) in actions) {\n                assets.add(index, item)\n                notifyItemInserted(index)\n            }\n        }\n\n        override fun commit(actions: List<Pair<Int, File>>) {\n            val groups = actions.map { it.second }.toTypedArray()\n            runOnDefaultDispatcher {\n                groups.forEach { it.deleteRecursively() }\n            }\n        }\n\n    }\n\n    val updating = AtomicInteger()\n\n    inner class AssetHolder(val binding: LayoutAssetItemBinding) : RecyclerView.ViewHolder(binding.root) {\n        lateinit var file: File\n\n        fun bind(file: File) {\n            this.file = file\n\n            binding.assetName.text = file.name\n            val versionFile = File(file.parentFile, \"${file.nameWithoutExtension}.version.txt\")\n\n            val localVersion = if (file.isFile) {\n                if (versionFile.isFile) {\n                    versionFile.readText().trim()\n                } else {\n                    DateFormat.getDateFormat(app).format(Date(file.lastModified()))\n                }\n            } else {\n                try {\n                    assets.open(\"v2ray/\" + versionFile.name).bufferedReader().readText().trim()\n                } catch (e: FileNotFoundException) {\n                    versionFile.readText()\n                    Logs.w(e)\n                    \"<unknown>\"\n                }\n            }\n\n            binding.assetStatus.text = getString(R.string.route_asset_status, localVersion)\n\n            binding.rulesUpdate.isInvisible = file.name !in internalFiles\n            binding.rulesUpdate.setOnClickListener {\n                updating.incrementAndGet()\n                layout.refreshLayout.isEnabled = false\n                binding.subscriptionUpdateProgress.isInvisible = false\n                binding.rulesUpdate.isInvisible = true\n                runOnDefaultDispatcher {\n                    runCatching {\n                        updateAsset(file, versionFile, localVersion)\n                    }.onFailure {\n                        onMainDispatcher {\n                            alert(it.readableMessage).show()\n                        }\n                    }\n\n                    onMainDispatcher {\n                        binding.rulesUpdate.isInvisible = false\n                        binding.subscriptionUpdateProgress.isInvisible = true\n                        if (updating.decrementAndGet() == 0) {\n                            layout.refreshLayout.isEnabled = true\n                        }\n                    }\n                }\n            }\n\n        }\n\n    }\n\n    suspend fun updateAsset(file: File, versionFile: File, localVersion: String) {\n        val okHttpClient = createProxyClient()\n\n        val repo: String\n        var fileName = file.name\n        if (DataStore.rulesProvider == 0) {\n            if (file.name == internalFiles[0]) {\n                repo = \"SagerNet/geoip\"\n            } else {\n                repo = \"v2fly/domain-list-community\"\n                fileName = \"dlc.dat\"\n            }\n            fileName = \"$fileName.xz\"\n        } else {\n            repo = \"Loyalsoldier/v2ray-rules-dat\"\n        }\n\n        var response = okHttpClient.newCall(\n            Request.Builder().url(\"https://api.github.com/repos/$repo/releases/latest\").build()\n        ).execute()\n\n        if (!response.isSuccessful) {\n            error(\"Error when fetching latest release of $repo : HTTP ${response.code}\\n\\n${response.body?.string()}\")\n        }\n\n        val release = JSONObject(response.body!!.string())\n        val tagName = release.getStr(\"tag_name\")\n\n        if (tagName == localVersion) {\n            onMainDispatcher {\n                snackbar(R.string.route_asset_no_update).show()\n            }\n            return\n        }\n\n        val releaseAssets = release.getJSONArray(\"assets\").filterIsInstance<JSONObject>()\n        val assetToDownload = releaseAssets.find { it.getStr(\"name\") == fileName }\n            ?: error(\"File $fileName not found in release ${release[\"url\"]}\")\n        val browserDownloadUrl = assetToDownload.getStr(\"browser_download_url\")\n\n        response = okHttpClient.newCall(\n            Request.Builder().url(browserDownloadUrl).build()\n        ).execute()\n\n        if (!response.isSuccessful) {\n            error(\"Error when downloading $browserDownloadUrl : HTTP ${response.code}\")\n        }\n\n        val cacheFile = File(file.parentFile, file.name + \".tmp\")\n        response.body!!.use { body ->\n            body.byteStream().use(cacheFile.outputStream())\n        }\n        if (fileName.endsWith(\".xz\")) {\n            Libcore.unxz(cacheFile.absolutePath, file.absolutePath)\n            cacheFile.delete()\n        } else {\n            cacheFile.renameTo(file)\n        }\n\n        versionFile.writeText(tagName)\n\n        adapter.reloadAssets()\n\n        onMainDispatcher {\n            snackbar(R.string.route_asset_updated).show()\n        }\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        finish()\n        return true\n    }\n\n    override fun onBackPressed() {\n        finish()\n    }\n\n    override fun onResume() {\n        super.onResume()\n\n        if (::adapter.isInitialized) {\n            adapter.reloadAssets()\n        }\n    }\n\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/CloudflareFragment.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.app.AlertDialog\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.databinding.LayoutCloudflareBinding\nimport io.nekohasekai.sagernet.databinding.LayoutProgressBinding\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.utils.Cloudflare\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.runBlocking\n\nclass CloudflareFragment : NamedFragment(R.layout.layout_cloudflare) {\n\n    override fun name() = \"CloudFlare\"\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val binding = LayoutCloudflareBinding.bind(view)\n        binding.warpGenerate.setOnClickListener {\n            runBlocking {\n                generateWarpConfiguration()\n            }\n        }\n    }\n\n    suspend fun generateWarpConfiguration() {\n        val activity = requireActivity() as MainActivity\n        val binding = LayoutProgressBinding.inflate(layoutInflater)\n        var job: Job? = null\n        val dialog = AlertDialog.Builder(requireContext())\n            .setView(binding.root)\n            .setCancelable(false)\n            .setNegativeButton(android.R.string.cancel) { _, _ ->\n                job?.cancel()\n            }\n            .show()\n        job = runOnDefaultDispatcher {\n            try {\n                val bean = Cloudflare.makeWireGuardConfiguration()\n                if (isActive) {\n                    val groupId = DataStore.selectedGroupForImport()\n                    if (DataStore.selectedGroup != groupId) {\n                        DataStore.selectedGroup = groupId\n                    }\n                    onMainDispatcher {\n                        activity.displayFragmentWithId(R.id.nav_configuration)\n                    }\n                    delay(1000L)\n                    onMainDispatcher {\n                        dialog.dismiss()\n                    }\n                    ProfileManager.createProfile(groupId, bean)\n                }\n            } catch (e: Exception) {\n                onMainDispatcher {\n                    if (isActive) {\n                        dialog.dismiss()\n                        activity.snackbar(e.readableMessage).show()\n                    }\n                }\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.annotation.SuppressLint\nimport android.content.ActivityNotFoundException\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.graphics.Color\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.SystemClock\nimport android.provider.OpenableColumns\nimport android.text.format.Formatter\nimport android.text.method.LinkMovementMethod\nimport android.text.util.Linkify\nimport android.util.TypedValue\nimport android.view.*\nimport android.widget.ImageView\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.graphics.TypefaceCompat\nimport androidx.core.view.isGone\nimport androidx.core.view.isVisible\nimport androidx.core.view.size\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport androidx.viewpager2.widget.ViewPager2\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.tabs.TabLayout\nimport com.google.android.material.tabs.TabLayoutMediator\nimport io.nekohasekai.sagernet.*\nimport io.nekohasekai.sagernet.aidl.TrafficStats\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.bg.test.LocalDnsInstance\nimport io.nekohasekai.sagernet.bg.test.UrlTest\nimport io.nekohasekai.sagernet.database.*\nimport io.nekohasekai.sagernet.databinding.LayoutProfileBinding\nimport io.nekohasekai.sagernet.databinding.LayoutProfileListBinding\nimport io.nekohasekai.sagernet.databinding.LayoutProgressListBinding\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.toUniversalLink\nimport io.nekohasekai.sagernet.fmt.v2ray.toV2rayN\nimport io.nekohasekai.sagernet.group.RawUpdater\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.plugin.PluginManager\nimport io.nekohasekai.sagernet.ui.profile.*\nimport io.nekohasekai.sagernet.widget.QRCodeDialog\nimport io.nekohasekai.sagernet.widget.UndoSnackbarManager\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport libcore.Libcore\nimport okhttp3.internal.closeQuietly\nimport java.net.InetAddress\nimport java.net.InetSocketAddress\nimport java.net.Socket\nimport java.net.UnknownHostException\nimport java.util.concurrent.ConcurrentLinkedQueue\nimport java.util.zip.ZipInputStream\n\nclass ConfigurationFragment @JvmOverloads constructor(\n    val select: Boolean = false,\n    val selectedItem: ProxyEntity? = null,\n) : ToolbarFragment(R.layout.layout_group_list),\n    PopupMenu.OnMenuItemClickListener,\n    Toolbar.OnMenuItemClickListener {\n\n    lateinit var adapter: GroupPagerAdapter\n    lateinit var tabLayout: TabLayout\n    lateinit var groupPager: ViewPager2\n    val selectedGroup get() = if (tabLayout.isGone) adapter.groupList[0] else adapter.groupList[tabLayout.selectedTabPosition]\n    val alwaysShowAddress by lazy { DataStore.alwaysShowAddress }\n    val securityAdvisory by lazy { DataStore.securityAdvisory }\n\n    val updateSelectedCallback = object : ViewPager2.OnPageChangeCallback() {\n        override fun onPageScrolled(\n            position: Int, positionOffset: Float, positionOffsetPixels: Int\n        ) {\n            if (adapter.groupList.size > position) {\n                DataStore.selectedGroup = adapter.groupList[position].id\n            }\n        }\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        if (!select) {\n            toolbar.inflateMenu(R.menu.add_profile_menu)\n            toolbar.setOnMenuItemClickListener(this)\n            runCatching {\n                val mTitleTextView = Toolbar::class.java.getDeclaredField(\"mTitleTextView\")\n                    .apply { isAccessible = true }.get(toolbar) as TextView\n                mTitleTextView.typeface = TypefaceCompat.createFromResourcesFontFile(\n                    view.context,\n                    resources,\n                    R.font.bgothm,\n                    \"res/font/bgothm.ttf\",\n                    mTitleTextView.typeface.style\n                )\n                mTitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,\n                    (mTitleTextView.textSize * 1.35).toFloat()\n                )\n            }.onFailure {\n                Logs.w(it)\n            }\n        } else {\n            toolbar.setTitle(R.string.select_profile)\n            toolbar.setNavigationIcon(R.drawable.ic_navigation_close)\n            toolbar.setNavigationOnClickListener {\n                requireActivity().finish()\n            }\n        }\n\n        groupPager = view.findViewById(R.id.group_pager)\n        tabLayout = view.findViewById(R.id.group_tab)\n        adapter = GroupPagerAdapter()\n        ProfileManager.addListener(adapter)\n        GroupManager.addListener(adapter)\n\n        groupPager.adapter = adapter\n        groupPager.offscreenPageLimit = 2\n\n        TabLayoutMediator(tabLayout, groupPager) { tab, position ->\n            if (adapter.groupList.size > position) {\n                tab.text = adapter.groupList[position].displayName()\n            }\n            tab.view.setOnLongClickListener { // clear toast\n                true\n            }\n        }.attach()\n\n        toolbar.setOnClickListener {\n\n            val fragment = (childFragmentManager.findFragmentByTag(\"f\" + selectedGroup.id) as GroupFragment?)\n\n            if (fragment != null) {\n                val selectedProxy = selectedItem?.id ?: DataStore.selectedProxy\n                val selectedProfileIndex = fragment.adapter.configurationIdList.indexOf(\n                    selectedProxy\n                )\n                if (selectedProfileIndex != -1) {\n                    val layoutManager = fragment.layoutManager\n                    val first = layoutManager.findFirstVisibleItemPosition()\n                    val last = layoutManager.findLastVisibleItemPosition()\n\n                    if (selectedProfileIndex !in first..last) {\n                        fragment.configurationListView.scrollTo(selectedProfileIndex, true)\n                        return@setOnClickListener\n                    }\n\n                }\n\n                fragment.configurationListView.scrollTo(0)\n            }\n\n        }\n    }\n\n    override fun onDestroy() {\n        if (::adapter.isInitialized) {\n            GroupManager.removeListener(adapter)\n            ProfileManager.removeListener(adapter)\n        }\n\n        super.onDestroy()\n    }\n\n    override fun onKeyDown(ketCode: Int, event: KeyEvent): Boolean {\n        val fragment = (childFragmentManager.findFragmentByTag(\"f\" + selectedGroup.id) as GroupFragment?)\n        fragment?.configurationListView?.apply {\n            if (!hasFocus()) requestFocus()\n        }\n        return super.onKeyDown(ketCode, event)\n    }\n\n    val importFile = registerForActivityResult(ActivityResultContracts.GetContent()) { file ->\n        if (file != null) runOnDefaultDispatcher {\n            try {\n                val fileName = requireContext().contentResolver.query(file, null, null, null, null)\n                    ?.use { cursor ->\n                        cursor.moveToFirst()\n                        cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)\n                            .let(cursor::getString)\n                    }\n\n                val proxies = mutableListOf<AbstractBean>()\n                if (fileName != null && fileName.endsWith(\".zip\")) {\n                    // try parse wireguard zip\n\n                    val zip = ZipInputStream(requireContext().contentResolver.openInputStream(file)!!)\n                    while (true) {\n                        val entry = zip.nextEntry ?: break\n                        if (entry.isDirectory) continue\n                        val fileText = zip.bufferedReader().readText()\n                        RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }\n                        zip.closeEntry()\n                    }\n                    zip.closeQuietly()\n                } else {\n                    val fileText = requireContext().contentResolver.openInputStream(file)!!.use {\n                        it.bufferedReader().readText()\n                    }\n                    RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }\n                }\n\n                if (proxies.isEmpty()) onMainDispatcher {\n                    snackbar(getString(R.string.no_proxies_found_in_file)).show()\n                } else import(proxies)\n            } catch (e: SubscriptionFoundException) {\n                (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))\n            } catch (e: Exception) {\n                Logs.w(e)\n\n                onMainDispatcher {\n                    snackbar(e.readableMessage).show()\n                }\n            }\n        }\n    }\n\n    suspend fun import(proxies: List<AbstractBean>) {\n        val targetId = DataStore.selectedGroupForImport()\n        val targetIndex = adapter.groupList.indexOfFirst { it.id == targetId }\n\n        for (proxy in proxies) {\n            ProfileManager.createProfile(targetId, proxy)\n        }\n        onMainDispatcher {\n            if (adapter.groupList.isEmpty() || selectedGroup.id != targetId) {\n                if (targetIndex != -1) {\n                    tabLayout.getTabAt(targetIndex)?.select()\n                } else {\n                    DataStore.selectedGroup = targetId\n                    adapter.reload()\n                }\n            }\n\n            snackbar(\n                requireContext().resources.getQuantityString(\n                    R.plurals.added, proxies.size, proxies.size\n                )\n            ).show()\n        }\n\n    }\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_scan_qr_code -> {\n                startActivity(Intent(context, ScannerActivity::class.java))\n            }\n            R.id.action_import_clipboard -> {\n                val text = SagerNet.getClipboardText()\n                if (text.isBlank()) {\n                    snackbar(getString(R.string.clipboard_empty)).show()\n                } else runOnDefaultDispatcher {\n                    try {\n                        val proxies = RawUpdater.parseRaw(text)\n                        if (proxies.isNullOrEmpty()) onMainDispatcher {\n                            snackbar(getString(R.string.no_proxies_found_in_clipboard)).show()\n                        } else import(proxies)\n                    } catch (e: SubscriptionFoundException) {\n                        (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))\n                    } catch (e: Exception) {\n                        Logs.w(e)\n\n                        onMainDispatcher {\n                            snackbar(e.readableMessage).show()\n                        }\n                    }\n                }\n            }\n            R.id.action_import_file -> {\n                startFilesForResult(importFile, \"*/*\")\n            }\n            R.id.action_new_socks -> {\n                startActivity(Intent(requireActivity(), SocksSettingsActivity::class.java))\n            }\n            R.id.action_new_http -> {\n                startActivity(Intent(requireActivity(), HttpSettingsActivity::class.java))\n            }\n            R.id.action_new_ss -> {\n                startActivity(Intent(requireActivity(), ShadowsocksSettingsActivity::class.java))\n            }\n            R.id.action_new_ssr -> {\n                startActivity(Intent(requireActivity(), ShadowsocksRSettingsActivity::class.java))\n            }\n            R.id.action_new_vmess -> {\n                startActivity(Intent(requireActivity(), VMessSettingsActivity::class.java))\n            }\n            R.id.action_new_vless -> {\n                startActivity(Intent(requireActivity(), VLESSSettingsActivity::class.java))\n            }\n            R.id.action_new_trojan -> {\n                startActivity(Intent(requireActivity(), TrojanSettingsActivity::class.java))\n            }\n            R.id.action_new_trojan_go -> {\n                startActivity(Intent(requireActivity(), TrojanGoSettingsActivity::class.java))\n            }\n            R.id.action_new_naive -> {\n                startActivity(Intent(requireActivity(), NaiveSettingsActivity::class.java))\n            }\n            R.id.action_new_ping_tunnel -> {\n                startActivity(Intent(requireActivity(), PingTunnelSettingsActivity::class.java))\n            }\n            R.id.action_new_relay_baton -> {\n                startActivity(Intent(requireActivity(), RelayBatonSettingsActivity::class.java))\n            }\n            R.id.action_new_brook -> {\n                startActivity(Intent(requireActivity(), BrookSettingsActivity::class.java))\n            }\n            R.id.action_new_hysteria -> {\n                startActivity(Intent(requireActivity(), HysteriaSettingsActivity::class.java))\n            }\n            R.id.action_new_snell -> {\n                startActivity(Intent(requireActivity(), SnellSettingsActivity::class.java))\n            }\n            R.id.action_new_ssh -> {\n                startActivity(Intent(requireActivity(), SSHSettingsActivity::class.java))\n            }\n            R.id.action_new_wg -> {\n                startActivity(Intent(requireActivity(), WireGuardSettingsActivity::class.java))\n            }\n            R.id.action_new_config -> {\n                startActivity(Intent(requireActivity(), ConfigSettingsActivity::class.java))\n            }\n            R.id.action_new_chain -> {\n                startActivity(Intent(requireActivity(), ChainSettingsActivity::class.java))\n            }\n            /*R.id.action_new_balancer -> {\n                startActivity(Intent(requireActivity(), BalancerSettingsActivity::class.java))\n            }*/\n            R.id.action_clear_traffic_statistics -> {\n                runOnDefaultDispatcher {\n                    val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())\n                    val toClear = mutableListOf<ProxyEntity>()\n                    if (profiles.isNotEmpty()) for (profile in profiles) {\n                        if (profile.tx != 0L || profile.rx != 0L) {\n                            profile.tx = 0\n                            profile.rx = 0\n                            toClear.add(profile)\n                        }\n                    }\n                    if (toClear.isNotEmpty()) {\n                        ProfileManager.updateProfile(toClear)\n                    }\n                }\n            }\n            R.id.action_connection_test_clear_results -> {\n                runOnDefaultDispatcher {\n                    val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())\n                    val toClear = mutableListOf<ProxyEntity>()\n                    if (profiles.isNotEmpty()) for (profile in profiles) {\n                        if (profile.status != 0) {\n                            profile.status = 0\n                            profile.ping = 0\n                            profile.error = null\n                            toClear.add(profile)\n                        }\n                    }\n                    if (toClear.isNotEmpty()) {\n                        ProfileManager.updateProfile(toClear)\n                    }\n                }\n            }\n            R.id.action_connection_icmp_ping -> {\n                pingTest(true)\n            }\n            R.id.action_connection_tcp_ping -> {\n                pingTest(false)\n            }\n            R.id.action_connection_url_test -> {\n                urlTest()\n            }\n            R.id.action_filter_groups -> {\n                runOnDefaultDispatcher filter@{\n                    val group = SagerDatabase.groupDao.getById(DataStore.currentGroupId())!!\n\n                    if (group.subscription?.type != SubscriptionType.OOCv1) {\n                        snackbar(getString(R.string.group_filter_ns)).show()\n                        return@filter\n                    }\n\n                    val subscription = group.subscription!!\n\n                    val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())\n                    val groups = profiles.mapNotNull { it.requireBean().group }\n                        .toSet()\n                        .toTypedArray()\n                    val checked = groups.map { it in subscription.selectedGroups }.toBooleanArray()\n\n                    if (groups.isEmpty()) {\n                        snackbar(getString(R.string.group_filter_groups_nf)).show()\n                        return@filter\n                    }\n\n                    onMainDispatcher {\n\n                        MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_groups)\n                            .setMultiChoiceItems(groups, checked) { _, which, isChecked ->\n                                val selected = groups[which]\n                                if (isChecked) {\n                                    subscription.selectedGroups.add(selected)\n                                } else {\n                                    subscription.selectedGroups.remove(selected)\n                                }\n                            }\n                            .setPositiveButton(android.R.string.ok) { _, _ ->\n                                runOnDefaultDispatcher {\n                                    GroupManager.updateGroup(group)\n                                }\n                            }\n                            .setNegativeButton(android.R.string.cancel, null)\n                            .show()\n\n                    }\n\n                }\n            }\n            R.id.group_filter_owners -> {\n                runOnDefaultDispatcher filter@{\n                    val group = SagerDatabase.groupDao.getById(DataStore.currentGroupId())!!\n\n                    if (group.subscription?.type != SubscriptionType.OOCv1) {\n                        snackbar(getString(R.string.group_filter_ns)).show()\n                        return@filter\n                    }\n\n                    val subscription = group.subscription!!\n\n                    val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.currentGroupId())\n                    val owners = profiles.mapNotNull { it.requireBean().owner }\n                        .toSet()\n                        .toTypedArray()\n                    val checked = owners.map { it in subscription.selectedOwners }.toBooleanArray()\n\n                    if (owners.isEmpty()) {\n                        snackbar(getString(R.string.group_filter_owners_nf)).show()\n                        return@filter\n                    }\n\n                    onMainDispatcher {\n\n                        MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_groups)\n                            .setMultiChoiceItems(owners, checked) { _, which, isChecked ->\n                                val selected = owners[which]\n                                if (isChecked) {\n                                    subscription.selectedOwners.add(selected)\n                                } else {\n                                    subscription.selectedOwners.remove(selected)\n                                }\n                            }\n                            .setPositiveButton(android.R.string.ok) { _, _ ->\n                                runOnDefaultDispatcher {\n                                    GroupManager.updateGroup(group)\n                                }\n                            }\n                            .setNegativeButton(android.R.string.cancel, null)\n                            .show()\n\n                    }\n\n                }\n            }\n            R.id.action_filter_tags -> {\n                runOnDefaultDispatcher filter@{\n                    val group = DataStore.currentGroup()\n                    if (group.subscription?.type != SubscriptionType.OOCv1) {\n                        snackbar(getString(R.string.group_filter_ns)).show()\n                        return@filter\n                    }\n\n                    val subscription = group.subscription!!\n                    val profiles = SagerDatabase.proxyDao.getByGroup(group.id)\n                    val groups = profiles.flatMap { it.requireBean().tags ?: listOf() }\n                        .toSet()\n                        .toTypedArray()\n                    val checked = groups.map { it in subscription.selectedTags }.toBooleanArray()\n                    if (groups.isEmpty()) {\n                        snackbar(getString(R.string.group_filter_tags_nf)).show()\n                        return@filter\n                    }\n\n                    onMainDispatcher {\n                        MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.group_filter_tags)\n                            .setMultiChoiceItems(groups, checked) { _, which, isChecked ->\n                                val selected = groups[which]\n                                if (isChecked) {\n                                    subscription.selectedTags.add(selected)\n                                } else {\n                                    subscription.selectedTags.remove(selected)\n                                }\n                            }\n                            .setPositiveButton(android.R.string.ok) { _, _ ->\n                                runOnDefaultDispatcher {\n                                    GroupManager.updateGroup(group)\n                                }\n                            }\n                            .setNegativeButton(android.R.string.cancel, null)\n                            .show()\n\n                    }\n\n                }\n            }\n        }\n        return true\n    }\n\n    inner class TestDialog {\n        val binding = LayoutProgressListBinding.inflate(layoutInflater)\n        val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)\n            .setNegativeButton(android.R.string.cancel) { _, _ ->\n                cancel()\n            }\n            .setCancelable(false)\n        lateinit var cancel: () -> Unit\n        val results = ArrayList<ProxyEntity>()\n        val adapter = TestAdapter()\n\n        suspend fun insert(profile: ProxyEntity) {\n            binding.listView.post {\n                results.add(profile)\n                adapter.notifyItemInserted(results.size - 1)\n                binding.listView.scrollToPosition(results.size - 1)\n            }\n        }\n\n        suspend fun update(profile: ProxyEntity) {\n            binding.listView.post {\n                val index = results.indexOf(profile)\n                adapter.notifyItemChanged(index)\n            }\n        }\n\n        init {\n            binding.listView.layoutManager = FixedLinearLayoutManager(binding.listView)\n            binding.listView.itemAnimator = DefaultItemAnimator()\n            binding.listView.adapter = adapter\n        }\n\n        inner class TestAdapter : RecyclerView.Adapter<TestResultHolder>() {\n            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =\n                TestResultHolder(LayoutProfileBinding.inflate(layoutInflater, parent, false))\n\n            override fun onBindViewHolder(holder: TestResultHolder, position: Int) {\n                holder.bind(results[position])\n            }\n\n            override fun getItemCount() = results.size\n        }\n\n        inner class TestResultHolder(val binding: LayoutProfileBinding) : RecyclerView.ViewHolder(\n            binding.root\n        ) {\n            init {\n                binding.edit.isGone = true\n                binding.share.isGone = true\n            }\n\n            fun bind(profile: ProxyEntity) {\n                binding.profileName.text = profile.displayName()\n                binding.profileType.text = profile.displayType()\n\n                when (profile.status) {\n                    -1 -> {\n                        binding.profileStatus.text = profile.error\n                        binding.profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))\n                    }\n                    0 -> {\n                        binding.profileStatus.setText(R.string.connection_test_testing)\n                        binding.profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))\n                    }\n                    1 -> {\n                        binding.profileStatus.text = getString(R.string.available, profile.ping)\n                        binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))\n                    }\n                    2 -> {\n                        binding.profileStatus.text = profile.error\n                        binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))\n                    }\n                    3 -> {\n                        binding.profileStatus.setText(R.string.unavailable)\n                        binding.profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))\n                    }\n                }\n\n                if (profile.status == 3) {\n                    binding.content.setOnClickListener {\n                        alert(profile.error ?: \"<?>\").show()\n                    }\n                } else {\n                    binding.content.setOnClickListener {}\n                }\n            }\n        }\n\n    }\n\n    fun stopService() {\n        if (SagerNet.started) SagerNet.stopService()\n    }\n\n    @Suppress(\"EXPERIMENTAL_API_USAGE\")\n    fun pingTest(icmpPing: Boolean) {\n        stopService()\n\n        val test = TestDialog()\n        val testJobs = mutableListOf<Job>()\n        val dialog = test.builder.show()\n        val mainJob = runOnDefaultDispatcher {\n            val group = DataStore.currentGroup()\n            var profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)\n            if (group.subscription?.type == SubscriptionType.OOCv1) {\n                val subscription = group.subscription!!\n                if (subscription.selectedGroups.isNotEmpty()) {\n                    profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().group in subscription.selectedGroups }\n                }\n                if (subscription.selectedOwners.isNotEmpty()) {\n                    profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().owner in subscription.selectedOwners }\n                }\n                if (subscription.selectedTags.isNotEmpty()) {\n                    profilesUnfiltered = profilesUnfiltered.filter { profile ->\n                        profile.requireBean().tags.containsAll(\n                            subscription.selectedTags\n                        )\n                    }\n                }\n            }\n            val profiles = ConcurrentLinkedQueue(profilesUnfiltered)\n            val testPool = newFixedThreadPoolContext(5, \"Connection test pool\")\n            repeat(5) {\n                testJobs.add(launch(testPool) {\n                    while (isActive) {\n                        val profile = profiles.poll() ?: break\n\n                        if (icmpPing) {\n                            if (!profile.requireBean().canICMPing()) {\n                                profile.status = -1\n                                profile.error = app.getString(R.string.connection_test_icmp_ping_unavailable)\n                                test.insert(profile)\n                                continue\n                            }\n                        } else {\n                            if (!profile.requireBean().canTCPing()) {\n                                profile.status = -1\n                                profile.error = app.getString(R.string.connection_test_tcp_ping_unavailable)\n                                test.insert(profile)\n                                continue\n                            }\n                        }\n\n                        profile.status = 0\n                        test.insert(profile)\n                        var address = profile.requireBean().serverAddress\n                        if (!address.isIpAddress()) {\n                            try {\n                                InetAddress.getAllByName(address).apply {\n                                    if (isNotEmpty()) {\n                                        address = this[0].hostAddress\n                                    }\n                                }\n                            } catch (ignored: UnknownHostException) {\n                            }\n                        }\n                        if (!isActive) break\n                        if (!address.isIpAddress()) {\n                            profile.status = 2\n                            profile.error = app.getString(R.string.connection_test_domain_not_found)\n                            test.update(profile)\n                            continue\n                        }\n                        try {\n                            if (icmpPing) {\n                                val result = Libcore.icmpPing(\n                                    address, 5000\n                                )\n                                if (!isActive) break\n                                if (result != -1) {\n                                    profile.status = 1\n                                    profile.ping = result\n                                } else {\n                                    profile.status = 2\n                                    profile.error = getString(R.string.connection_test_unreachable)\n                                }\n                                test.update(profile)\n                            } else {\n                                val socket = Socket()\n                                try {\n                                    socket.soTimeout = 5000\n                                    socket.bind(InetSocketAddress(0))\n                                    protectFromVpn(socket.fileDescriptor.int)\n                                    val start = SystemClock.elapsedRealtime()\n                                    socket.connect(\n                                        InetSocketAddress(\n                                            address, profile.requireBean().serverPort\n                                        ), 5000\n                                    )\n                                    if (!isActive) break\n                                    profile.status = 1\n                                    profile.ping = (SystemClock.elapsedRealtime() - start).toInt()\n                                    test.update(profile)\n                                } finally {\n                                    socket.closeQuietly()\n                                }\n                            }\n                        } catch (e: Exception) {\n                            if (!isActive) break\n                            val message = e.readableMessage\n\n                            if (icmpPing) {\n                                profile.status = 2\n                                profile.error = getString(R.string.connection_test_unreachable)\n                            } else {\n                                profile.status = 2\n                                when {\n                                    !message.contains(\"failed:\") -> profile.error = getString(R.string.connection_test_timeout)\n                                    else -> when {\n                                        message.contains(\"ECONNREFUSED\") -> {\n                                            profile.error = getString(R.string.connection_test_refused)\n                                        }\n                                        message.contains(\"ENETUNREACH\") -> {\n                                            profile.error = getString(R.string.connection_test_unreachable)\n                                        }\n                                        else -> {\n                                            profile.status = 3\n                                            profile.error = message\n                                        }\n                                    }\n                                }\n                            }\n                            test.update(profile)\n                        }\n                    }\n                })\n            }\n\n            testJobs.joinAll()\n            testPool.close()\n\n            ProfileManager.updateProfile(test.results.filter { it.status != 0 })\n\n            onMainDispatcher {\n                test.binding.progressCircular.isGone = true\n                dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)\n            }\n        }\n        test.cancel = {\n            mainJob.cancel()\n            testJobs.forEach { it.cancel() }\n            runOnDefaultDispatcher {\n                ProfileManager.updateProfile(test.results.filter { it.status != 0 })\n            }\n        }\n    }\n\n    @Suppress(\"EXPERIMENTAL_API_USAGE\")\n    fun urlTest() {\n        stopService()\n\n        val test = TestDialog()\n        val dialog = test.builder.show()\n        val testJobs = mutableListOf<Job>()\n        val dnsInstance = LocalDnsInstance()\n\n        val mainJob = runOnDefaultDispatcher {\n            val group = DataStore.currentGroup()\n            var profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)\n            if (group.subscription?.type == SubscriptionType.OOCv1) {\n                val subscription = group.subscription!!\n                if (subscription.selectedGroups.isNotEmpty()) {\n                    profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().group in subscription.selectedGroups }\n                }\n                if (subscription.selectedOwners.isNotEmpty()) {\n                    profilesUnfiltered = profilesUnfiltered.filter { it.requireBean().owner in subscription.selectedOwners }\n                }\n                if (subscription.selectedTags.isNotEmpty()) {\n                    profilesUnfiltered = profilesUnfiltered.filter { profile ->\n                        profile.requireBean().tags.containsAll(\n                            subscription.selectedTags\n                        )\n                    }\n                }\n            }\n            val profiles = ConcurrentLinkedQueue(profilesUnfiltered)\n            val urlTest = UrlTest()\n            dnsInstance.launch()\n\n            repeat(5) {\n                testJobs.add(launch {\n                    while (isActive) {\n                        val profile = profiles.poll() ?: break\n                        profile.status = 0\n                        test.insert(profile)\n\n                        try {\n                            val result = urlTest.doTest(profile)\n                            profile.status = 1\n                            profile.ping = result\n                        } catch (e: PluginManager.PluginNotFoundException) {\n                            profile.status = 2\n                            profile.error = e.readableMessage\n                        } catch (e: Exception) {\n                            profile.status = 3\n                            profile.error = e.readableMessage\n                        }\n\n                        test.update(profile)\n                        ProfileManager.updateProfile(profile)\n                    }\n                })\n            }\n\n            testJobs.joinAll()\n            dnsInstance.closeQuietly()\n            onMainDispatcher {\n                test.binding.progressCircular.isGone = true\n                dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)\n            }\n        }\n        test.cancel = {\n            dnsInstance.closeQuietly()\n            mainJob.cancel()\n            runOnDefaultDispatcher {\n                GroupManager.postReload(DataStore.currentGroupId())\n            }\n        }\n    }\n\n    inner class GroupPagerAdapter : FragmentStateAdapter(this),\n        ProfileManager.Listener,\n        GroupManager.Listener {\n\n        var selectedGroupIndex = 0\n        var groupList: ArrayList<ProxyGroup> = ArrayList()\n\n        fun reload() {\n\n            if (!select) {\n                groupPager.unregisterOnPageChangeCallback(updateSelectedCallback)\n            }\n\n            runOnDefaultDispatcher {\n                groupList = ArrayList(SagerDatabase.groupDao.allGroups())\n                if (groupList.isEmpty()) {\n                    SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true))\n                    groupList = ArrayList(SagerDatabase.groupDao.allGroups())\n                }\n\n                val hideUngrouped = groupList.size > 1 && SagerDatabase.proxyDao.countByGroup(\n                    groupList.find { it.ungrouped }!!.id\n                ) == 0L\n\n                if (hideUngrouped) groupList.removeAll { it.ungrouped }\n\n                val selectedGroup = selectedItem?.groupId ?: DataStore.currentGroupId()\n                if (selectedGroup != 0L) {\n                    val selectedIndex = groupList.indexOfFirst { it.id == selectedGroup }\n                    selectedGroupIndex = selectedIndex\n\n                    onMainDispatcher {\n                        groupPager.setCurrentItem(selectedIndex, false)\n                    }\n                }\n\n                groupPager.post {\n                    if (!select) {\n                        groupPager.registerOnPageChangeCallback(updateSelectedCallback)\n                    }\n                }\n\n                onMainDispatcher {\n                    notifyDataSetChanged()\n                    val hideTab = groupList.size < 2\n                    tabLayout.isGone = hideTab\n                    toolbar.elevation = if (hideTab) 0F else dp2px(4).toFloat()\n                }\n            }\n        }\n\n        init {\n            reload()\n        }\n\n        override fun getItemCount(): Int {\n            return groupList.size\n        }\n\n        override fun createFragment(position: Int): Fragment {\n            return GroupFragment().apply {\n                proxyGroup = groupList[position]\n                if (position == selectedGroupIndex) {\n                    selected = true\n                }\n            }\n        }\n\n        override fun getItemId(position: Int): Long {\n            return groupList[position].id\n        }\n\n        override fun containsItem(itemId: Long): Boolean {\n            return groupList.any { it.id == itemId }\n        }\n\n        override suspend fun groupAdd(group: ProxyGroup) {\n            tabLayout.post {\n                groupList.add(group)\n\n                if (groupList.any { !it.ungrouped }) tabLayout.post {\n                    tabLayout.visibility = View.VISIBLE\n                }\n\n                notifyItemInserted(groupList.size - 1)\n                tabLayout.getTabAt(groupList.size - 1)?.select()\n            }\n        }\n\n        override suspend fun groupRemoved(groupId: Long) {\n            val index = groupList.indexOfFirst { it.id == groupId }\n            if (index == -1) return\n\n            tabLayout.post {\n                groupList.removeAt(index)\n                notifyItemRemoved(index)\n            }\n        }\n\n        override suspend fun groupUpdated(group: ProxyGroup) {\n            val index = groupList.indexOfFirst { it.id == group.id }\n            if (index == -1) return\n\n            tabLayout.post {\n                tabLayout.getTabAt(index)?.text = group.displayName()\n            }\n        }\n\n        override suspend fun groupUpdated(groupId: Long) = Unit\n\n        override suspend fun onAdd(profile: ProxyEntity) {\n            if (groupList.find { it.id == profile.groupId } == null) {\n                DataStore.selectedGroup = profile.groupId\n                reload()\n            }\n        }\n\n        override suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats) = Unit\n\n        override suspend fun onUpdated(profile: ProxyEntity) = Unit\n\n        override suspend fun onRemoved(groupId: Long, profileId: Long) {\n            val group = groupList.find { it.id == groupId } ?: return\n            if (group.ungrouped && SagerDatabase.proxyDao.countByGroup(groupId) == 0L) {\n                reload()\n            }\n        }\n    }\n\n    class GroupFragment : Fragment() {\n\n        lateinit var proxyGroup: ProxyGroup\n        var selected = false\n        var scrolled = false\n\n        override fun onCreateView(\n            inflater: LayoutInflater,\n            container: ViewGroup?,\n            savedInstanceState: Bundle?,\n        ): View? {\n            return LayoutProfileListBinding.inflate(inflater).root\n        }\n\n        lateinit var undoManager: UndoSnackbarManager<ProxyEntity>\n        lateinit var adapter: ConfigurationAdapter\n\n        override fun onSaveInstanceState(outState: Bundle) {\n            super.onSaveInstanceState(outState)\n\n            if (::proxyGroup.isInitialized) {\n                outState.putParcelable(\"proxyGroup\", proxyGroup)\n            }\n        }\n\n        override fun onViewStateRestored(savedInstanceState: Bundle?) {\n            super.onViewStateRestored(savedInstanceState)\n\n            savedInstanceState?.getParcelable<ProxyGroup>(\"proxyGroup\")?.also {\n                proxyGroup = it\n                onViewCreated(requireView(), null)\n            }\n        }\n\n        private val isEnabled: Boolean\n            get() {\n                return ((activity as? MainActivity)\n                    ?: return false).state.let { it.canStop || it == BaseService.State.Stopped }\n            }\n\n        private fun isProfileEditable(id: Long): Boolean {\n            return ((activity as? MainActivity)\n                ?: return false).state == BaseService.State.Stopped || id != DataStore.selectedProxy\n        }\n\n        lateinit var layoutManager: LinearLayoutManager\n        lateinit var configurationListView: RecyclerView\n\n        val select by lazy { (parentFragment as ConfigurationFragment).select }\n        val selectedItem by lazy { (parentFragment as ConfigurationFragment).selectedItem }\n\n        override fun onResume() {\n            super.onResume()\n\n            if (::configurationListView.isInitialized && configurationListView.size == 0) {\n                configurationListView.adapter = adapter\n                runOnDefaultDispatcher {\n                    adapter.reloadProfiles()\n                }\n            } else if (!::configurationListView.isInitialized) {\n                onViewCreated(requireView(), null)\n            }\n            checkOrderMenu()\n            configurationListView.requestFocus()\n        }\n\n        fun checkOrderMenu() {\n            if (select) return\n\n            val pf = requireParentFragment() as? ToolbarFragment ?: return\n            val menu = pf.toolbar.menu\n            val origin = menu.findItem(R.id.action_order_origin)\n            val byName = menu.findItem(R.id.action_order_by_name)\n            val byDelay = menu.findItem(R.id.action_order_by_delay)\n            when (proxyGroup.order) {\n                GroupOrder.ORIGIN -> {\n                    origin.isChecked = true\n                }\n                GroupOrder.BY_NAME -> {\n                    byName.isChecked = true\n                }\n                GroupOrder.BY_DELAY -> {\n                    byDelay.isChecked = true\n                }\n            }\n\n            fun updateTo(order: Int) {\n                if (proxyGroup.order == order) return\n                runOnDefaultDispatcher {\n                    proxyGroup.order = order\n                    GroupManager.updateGroup(proxyGroup)\n                }\n            }\n\n            origin.setOnMenuItemClickListener {\n                it.isChecked = true\n                updateTo(GroupOrder.ORIGIN)\n                true\n            }\n            byName.setOnMenuItemClickListener {\n                it.isChecked = true\n                updateTo(GroupOrder.BY_NAME)\n                true\n            }\n            byDelay.setOnMenuItemClickListener {\n                it.isChecked = true\n                updateTo(GroupOrder.BY_DELAY)\n                true\n            }\n        }\n\n        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n            if (!::proxyGroup.isInitialized) return\n\n            configurationListView = view.findViewById(R.id.configuration_list)\n            layoutManager = FixedLinearLayoutManager(configurationListView)\n            configurationListView.layoutManager = layoutManager\n            adapter = ConfigurationAdapter()\n            ProfileManager.addListener(adapter)\n            GroupManager.addListener(adapter)\n            configurationListView.adapter = adapter\n            configurationListView.setItemViewCacheSize(20)\n\n            if (!select && proxyGroup.type == GroupType.BASIC) {\n\n                undoManager = UndoSnackbarManager(activity as MainActivity, adapter)\n\n                ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(\n                    ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START\n                ) {\n                    override fun getSwipeDirs(\n                        recyclerView: RecyclerView,\n                        viewHolder: RecyclerView.ViewHolder,\n                    ): Int {\n                        return if (isProfileEditable((viewHolder as ConfigurationHolder).entity.id)) {\n                            super.getSwipeDirs(recyclerView, viewHolder)\n                        } else 0\n                    }\n\n                    override fun getDragDirs(\n                        recyclerView: RecyclerView,\n                        viewHolder: RecyclerView.ViewHolder,\n                    ) = if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0\n\n                    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n                        val index = viewHolder.bindingAdapterPosition\n                        adapter.remove(index)\n                        undoManager.remove(index to (viewHolder as ConfigurationHolder).entity)\n                    }\n\n                    override fun onMove(\n                        recyclerView: RecyclerView,\n                        viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,\n                    ): Boolean {\n                        adapter.move(\n                            viewHolder.bindingAdapterPosition, target.bindingAdapterPosition\n                        )\n                        return true\n                    }\n\n                    override fun clearView(\n                        recyclerView: RecyclerView,\n                        viewHolder: RecyclerView.ViewHolder,\n                    ) {\n                        super.clearView(recyclerView, viewHolder)\n                        adapter.commitMove()\n                    }\n                }).attachToRecyclerView(configurationListView)\n\n            }\n\n        }\n\n        override fun onDestroy() {\n            if (::adapter.isInitialized) {\n                ProfileManager.removeListener(adapter)\n                GroupManager.removeListener(adapter)\n            }\n\n            super.onDestroy()\n\n            if (!::undoManager.isInitialized) return\n            undoManager.flush()\n        }\n\n        inner class ConfigurationAdapter : RecyclerView.Adapter<ConfigurationHolder>(),\n            ProfileManager.Listener,\n            GroupManager.Listener,\n            UndoSnackbarManager.Interface<ProxyEntity> {\n\n            init {\n                setHasStableIds(true)\n            }\n\n            var configurationIdList: MutableList<Long> = mutableListOf()\n            val configurationList = HashMap<Long, ProxyEntity>()\n\n            private fun getItem(profileId: Long): ProxyEntity {\n                var profile = configurationList[profileId]\n                if (profile == null) {\n                    profile = ProfileManager.getProfile(profileId)\n                    if (profile != null) {\n                        configurationList[profileId] = profile\n                    }\n                }\n                return profile!!\n            }\n\n            private fun getItemAt(index: Int) = getItem(configurationIdList[index])\n\n            override fun onCreateViewHolder(\n                parent: ViewGroup,\n                viewType: Int,\n            ): ConfigurationHolder {\n                return ConfigurationHolder(\n                    LayoutInflater.from(parent.context)\n                        .inflate(R.layout.layout_profile, parent, false)\n                )\n            }\n\n            override fun getItemId(position: Int): Long {\n                return configurationIdList[position]\n            }\n\n            override fun onBindViewHolder(holder: ConfigurationHolder, position: Int) {\n                try {\n                    holder.bind(getItemAt(position))\n                } catch (ignored: NullPointerException) { // when group deleted\n                }\n            }\n\n            override fun getItemCount(): Int {\n                return configurationIdList.size\n            }\n\n            private val updated = HashSet<ProxyEntity>()\n\n            fun move(from: Int, to: Int) {\n                val first = getItemAt(from)\n                var previousOrder = first.userOrder\n                val (step, range) = if (from < to) Pair(1, from until to) else Pair(\n                    -1, to + 1 downTo from\n                )\n                for (i in range) {\n                    val next = getItemAt(i + step)\n                    val order = next.userOrder\n                    next.userOrder = previousOrder\n                    previousOrder = order\n                    configurationIdList[i] = next.id\n                    updated.add(next)\n                }\n                first.userOrder = previousOrder\n                configurationIdList[to] = first.id\n                updated.add(first)\n                notifyItemMoved(from, to)\n            }\n\n            fun commitMove() = runOnDefaultDispatcher {\n                updated.forEach { SagerDatabase.proxyDao.updateProxy(it) }\n                updated.clear()\n            }\n\n            fun remove(pos: Int) {\n                configurationIdList.removeAt(pos)\n                notifyItemRemoved(pos)\n            }\n\n            override fun undo(actions: List<Pair<Int, ProxyEntity>>) {\n                for ((index, item) in actions) {\n                    configurationListView.post {\n                        configurationList[item.id] = item\n                        configurationIdList.add(index, item.id)\n                        notifyItemInserted(index)\n                    }\n                }\n            }\n\n            override fun commit(actions: List<Pair<Int, ProxyEntity>>) {\n                val profiles = actions.map { it.second }\n                runOnDefaultDispatcher {\n                    for (entity in profiles) {\n                        ProfileManager.deleteProfile(entity.groupId, entity.id)\n                    }\n                }\n            }\n\n            override suspend fun onAdd(profile: ProxyEntity) {\n                if (profile.groupId != proxyGroup.id) return\n\n                configurationListView.post {\n                    if (::undoManager.isInitialized) {\n                        undoManager.flush()\n                    }\n                    val pos = itemCount\n                    configurationList[profile.id] = profile\n                    configurationIdList.add(profile.id)\n                    notifyItemInserted(pos)\n                }\n            }\n\n            override suspend fun onUpdated(profile: ProxyEntity) {\n                if (profile.groupId != proxyGroup.id) return\n                val index = configurationIdList.indexOf(profile.id)\n                if (index < 0) return\n                configurationListView.post {\n                    if (::undoManager.isInitialized) {\n                        undoManager.flush()\n                    }\n                    configurationList[profile.id] = profile\n                    notifyItemChanged(index)\n                }\n            }\n\n            override suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats) {\n                val index = configurationIdList.indexOf(profileId)\n                if (index != -1) {\n                    val holder = layoutManager.findViewByPosition(index)\n                        ?.let { configurationListView.getChildViewHolder(it) } as ConfigurationHolder?\n                    if (holder != null) {\n                        holder.entity.stats = trafficStats\n                        onMainDispatcher {\n                            holder.bind(holder.entity)\n                        }\n                    }\n                }\n            }\n\n            override suspend fun onRemoved(groupId: Long, profileId: Long) {\n                if (groupId != proxyGroup.id) return\n                val index = configurationIdList.indexOf(profileId)\n                if (index < 0) return\n\n                configurationListView.post {\n                    configurationIdList.removeAt(index)\n                    configurationList.remove(profileId)\n                    notifyItemRemoved(index)\n                }\n            }\n\n            override suspend fun groupAdd(group: ProxyGroup) = Unit\n            override suspend fun groupRemoved(groupId: Long) = Unit\n\n            override suspend fun groupUpdated(group: ProxyGroup) {\n                if (group.id != proxyGroup.id) return\n                proxyGroup = group\n                reloadProfiles()\n            }\n\n            override suspend fun groupUpdated(groupId: Long) {\n                if (groupId != proxyGroup.id) return\n                proxyGroup = SagerDatabase.groupDao.getById(groupId)!!\n                reloadProfiles()\n            }\n\n            fun reloadProfiles() {\n                var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)\n                val subscription = proxyGroup.subscription\n                if (subscription != null) {\n                    if (subscription.selectedGroups.isNotEmpty()) {\n                        newProfiles = newProfiles.filter { it.requireBean().group in subscription.selectedGroups }\n                    }\n                    if (subscription.selectedOwners.isNotEmpty()) {\n                        newProfiles = newProfiles.filter { it.requireBean().owner in subscription.selectedOwners }\n                    }\n                    if (subscription.selectedTags.isNotEmpty()) {\n                        newProfiles = newProfiles.filter { profile ->\n                            profile.requireBean().tags.containsAll(\n                                subscription.selectedTags\n                            )\n                        }\n                    }\n                }\n                when (proxyGroup.order) {\n                    GroupOrder.BY_NAME -> {\n                        newProfiles = newProfiles.sortedBy { it.displayName() }\n\n                    }\n                    GroupOrder.BY_DELAY -> {\n                        newProfiles = newProfiles.sortedBy { if (it.status == 1) it.ping else 114514 }\n                    }\n                }\n\n                configurationList.clear()\n                configurationList.putAll(newProfiles.associateBy { it.id })\n                val newProfileIds = newProfiles.map { it.id }\n\n                var selectedProfileIndex = -1\n\n                if (selected) {\n                    val selectedProxy = selectedItem?.id ?: DataStore.selectedProxy\n                    selectedProfileIndex = newProfileIds.indexOf(selectedProxy)\n                }\n\n                configurationListView.post {\n                    configurationIdList.clear()\n                    configurationIdList.addAll(newProfileIds)\n                    notifyDataSetChanged()\n\n                    if (selectedProfileIndex != -1) {\n                        configurationListView.scrollTo(selectedProfileIndex, true)\n                    } else if (newProfiles.isNotEmpty()) {\n                        configurationListView.scrollTo(0, true)\n                    }\n\n                }\n            }\n\n        }\n\n        val profileAccess = Mutex()\n        val reloadAccess = Mutex()\n\n        inner class ConfigurationHolder(val view: View) : RecyclerView.ViewHolder(view),\n            PopupMenu.OnMenuItemClickListener {\n\n            lateinit var entity: ProxyEntity\n\n            val profileName: TextView = view.findViewById(R.id.profile_name)\n            val profileType: TextView = view.findViewById(R.id.profile_type)\n            val profileAddress: TextView = view.findViewById(R.id.profile_address)\n            val profileStatus: TextView = view.findViewById(R.id.profile_status)\n\n            val trafficText: TextView = view.findViewById(R.id.traffic_text)\n            val selectedView: LinearLayout = view.findViewById(R.id.selected_view)\n            val editButton: ImageView = view.findViewById(R.id.edit)\n            val shareLayout: LinearLayout = view.findViewById(R.id.share)\n            val shareLayer: LinearLayout = view.findViewById(R.id.share_layer)\n            val shareButton: ImageView = view.findViewById(R.id.shareIcon)\n\n            fun bind(proxyEntity: ProxyEntity) {\n                val pf = requireParentFragment() as? ConfigurationFragment ?: return\n\n                entity = proxyEntity\n\n                if (select) {\n                    view.setOnClickListener {\n                        (requireActivity() as ProfileSelectActivity).returnProfile(proxyEntity.id)\n                    }\n                } else {\n                    val pa = activity as MainActivity\n\n                    view.setOnClickListener {\n                        runOnDefaultDispatcher {\n                            var update: Boolean\n                            var lastSelected: Long\n                            profileAccess.withLock {\n                                update = DataStore.selectedProxy != proxyEntity.id\n                                lastSelected = DataStore.selectedProxy\n                                DataStore.selectedProxy = proxyEntity.id\n                                onMainDispatcher {\n                                    selectedView.visibility = View.VISIBLE\n                                }\n                            }\n\n                            if (update) {\n                                ProfileManager.postUpdate(lastSelected)\n                                if (pa.state.canStop && reloadAccess.tryLock()) {\n                                    SagerNet.stopService()\n                                    delay(1000L)\n                                    SagerNet.startService()\n                                    reloadAccess.unlock()\n                                }\n                            } else if (SagerNet.isTv) {\n                                if (SagerNet.started) {\n                                    SagerNet.stopService()\n                                } else {\n                                    SagerNet.startService()\n                                }\n                            }\n                        }\n\n                    }\n                }\n\n                profileName.text = proxyEntity.displayName()\n                profileType.text = proxyEntity.displayType()\n\n                var rx = proxyEntity.rx\n                var tx = proxyEntity.tx\n\n                val stats = proxyEntity.stats\n                if (stats != null) {\n                    rx += stats.rxTotal\n                    tx += stats.txTotal\n                }\n\n                val showTraffic = rx + tx != 0L\n                trafficText.isVisible = showTraffic\n                if (showTraffic) {\n                    trafficText.text = view.context.getString(\n                        R.string.traffic,\n                        Formatter.formatFileSize(view.context, tx),\n                        Formatter.formatFileSize(view.context, rx)\n                    )\n                }\n\n                var address = proxyEntity.displayAddress()\n                if (showTraffic && address.length >= 30) {\n                    address = address.substring(0, 27) + \"...\"\n                }\n\n                if (proxyEntity.requireBean().name.isBlank() || !pf.alwaysShowAddress) {\n                    address = \"\"\n                }\n\n                profileAddress.text = address\n                (trafficText.parent as View).isGone = (!showTraffic || proxyEntity.status <= 0) && address.isBlank()\n\n                if (proxyEntity.status <= 0) {\n                    if (showTraffic) {\n                        profileStatus.text = trafficText.text\n                        profileStatus.setTextColor(requireContext().getColorAttr(android.R.attr.textColorSecondary))\n                        trafficText.text = \"\"\n                    } else {\n                        profileStatus.text = \"\"\n                    }\n                } else if (proxyEntity.status == 1) {\n                    profileStatus.text = getString(R.string.available, proxyEntity.ping)\n                    profileStatus.setTextColor(requireContext().getColour(R.color.material_green_500))\n                } else {\n                    profileStatus.setTextColor(requireContext().getColour(R.color.material_red_500))\n                    if (proxyEntity.status == 2) {\n                        profileStatus.text = proxyEntity.error\n                    }\n                }\n\n                if (proxyEntity.status == 3) {\n                    profileStatus.setText(R.string.unavailable)\n                    profileStatus.setOnClickListener {\n                        alert(proxyEntity.error ?: \"<?>\").show()\n                    }\n                } else {\n                    profileStatus.setOnClickListener(null)\n                }\n\n                editButton.setOnClickListener {\n                    it.context.startActivity(\n                        proxyEntity.settingIntent(\n                            it.context, proxyGroup.type == GroupType.SUBSCRIPTION\n                        )\n                    )\n                }\n\n                shareLayout.isGone = proxyEntity.type == ProxyEntity.TYPE_CHAIN\n                editButton.isGone = select\n\n                runOnDefaultDispatcher {\n                    val selected = (selectedItem?.id ?: DataStore.selectedProxy) == proxyEntity.id\n                    val started = selected && SagerNet.started && DataStore.currentProfile == proxyEntity.id\n                    onMainDispatcher {\n                        editButton.isEnabled = !started\n                        selectedView.visibility = if (selected) View.VISIBLE else View.INVISIBLE\n                    }\n\n                    fun showShare(anchor: View) {\n                        val popup = PopupMenu(requireContext(), anchor)\n                        popup.menuInflater.inflate(R.menu.profile_share_menu, popup.menu)\n\n                        if (proxyEntity.vmessBean == null) {\n                            popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_v2rayn_qr)\n                            popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(R.id.action_v2rayn_clipboard)\n                        }\n\n                        when {\n                            !proxyEntity.haveStandardLink() -> {\n                                popup.menu.findItem(R.id.action_group_qr).subMenu.removeItem(R.id.action_standard_qr)\n                                popup.menu.findItem(R.id.action_group_clipboard).subMenu.removeItem(\n                                    R.id.action_standard_clipboard\n                                )\n                            }\n                            !proxyEntity.haveLink() -> {\n                                popup.menu.removeItem(R.id.action_group_qr)\n                                popup.menu.removeItem(R.id.action_group_clipboard)\n                            }\n                        }\n\n                        if (proxyEntity.ptBean != null || proxyEntity.brookBean != null) {\n                            popup.menu.removeItem(R.id.action_group_configuration)\n                        }\n\n                        popup.setOnMenuItemClickListener(this@ConfigurationHolder)\n                        popup.show()\n                    }\n\n                    if (!(select || proxyEntity.type == ProxyEntity.TYPE_CHAIN)) {\n\n                        val validateResult = if (pf.securityAdvisory) {\n                            proxyEntity.requireBean().isInsecure()\n                        } else ResultLocal\n\n                        when (validateResult) {\n                            is ResultInsecure -> onMainDispatcher {\n                                shareLayout.isVisible = true\n\n                                shareLayer.setBackgroundColor(Color.RED)\n                                shareButton.setImageResource(R.drawable.ic_baseline_warning_24)\n                                shareButton.setColorFilter(Color.WHITE)\n\n                                shareLayout.setOnClickListener {\n                                    MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.insecure)\n                                        .setMessage(resources.openRawResource(validateResult.textRes)\n                                            .bufferedReader()\n                                            .use { it.readText() })\n                                        .setPositiveButton(android.R.string.ok) { _, _ ->\n                                            showShare(it)\n                                        }\n                                        .show()\n                                        .apply {\n                                            findViewById<TextView>(android.R.id.message)?.apply {\n                                                Linkify.addLinks(this, Linkify.WEB_URLS)\n                                                movementMethod = LinkMovementMethod.getInstance()\n                                            }\n                                        }\n                                }\n                            }\n                            is ResultDeprecated -> onMainDispatcher {\n                                shareLayout.isVisible = true\n\n                                shareLayer.setBackgroundColor(Color.YELLOW)\n                                shareButton.setImageResource(R.drawable.ic_baseline_warning_24)\n                                shareButton.setColorFilter(Color.GRAY)\n\n                                shareLayout.setOnClickListener {\n                                    MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.deprecated)\n                                        .setMessage(resources.openRawResource(validateResult.textRes)\n                                            .bufferedReader()\n                                            .use { it.readText() })\n                                        .setPositiveButton(android.R.string.ok) { _, _ ->\n                                            showShare(it)\n                                        }\n                                        .show()\n                                        .apply {\n                                            findViewById<TextView>(android.R.id.message)?.apply {\n                                                Linkify.addLinks(this, Linkify.WEB_URLS)\n                                                movementMethod = LinkMovementMethod.getInstance()\n                                            }\n                                        }\n                                }\n                            }\n                            else -> onMainDispatcher {\n                                shareLayer.setBackgroundColor(Color.TRANSPARENT)\n                                shareButton.setImageResource(R.drawable.ic_social_share)\n                                shareButton.setColorFilter(Color.GRAY)\n                                shareButton.isVisible = true\n\n                                shareLayout.setOnClickListener {\n                                    showShare(it)\n                                }\n                            }\n                        }\n                    }\n                }\n\n            }\n\n            fun showCode(link: String) {\n                QRCodeDialog(link).showAllowingStateLoss(parentFragmentManager)\n            }\n\n            fun export(link: String) {\n                val success = SagerNet.trySetPrimaryClip(link)\n                (activity as MainActivity).snackbar(if (success) R.string.action_export_msg else R.string.action_export_err)\n                    .show()\n            }\n\n            override fun onMenuItemClick(item: MenuItem): Boolean {\n                try {\n                    when (item.itemId) {\n                        R.id.action_standard_qr -> showCode(entity.toLink()!!)\n                        R.id.action_standard_clipboard -> export(entity.toLink()!!)\n                        R.id.action_universal_qr -> showCode(entity.requireBean().toUniversalLink())\n                        R.id.action_universal_clipboard -> export(\n                            entity.requireBean().toUniversalLink()\n                        )\n                        R.id.action_v2rayn_qr -> showCode(entity.vmessBean!!.toV2rayN())\n                        R.id.action_v2rayn_clipboard -> export(entity.vmessBean!!.toV2rayN())\n                        R.id.action_config_export_clipboard -> export(entity.exportConfig().first)\n                        R.id.action_config_export_file -> {\n                            val cfg = entity.exportConfig()\n                            DataStore.serverConfig = cfg.first\n                            startFilesForResult(\n                                (parentFragment as ConfigurationFragment).exportConfig, cfg.second\n                            )\n                        }\n                    }\n                } catch (e: Exception) {\n                    Logs.w(e)\n                    (activity as MainActivity).snackbar(e.readableMessage).show()\n                    return true\n                }\n                return true\n            }\n        }\n\n    }\n\n    private val exportConfig = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->\n        if (data != null) {\n            runOnDefaultDispatcher {\n                try {\n                    (requireActivity() as MainActivity).contentResolver.openOutputStream(data)!!\n                        .bufferedWriter()\n                        .use {\n                            it.write(DataStore.serverConfig)\n                        }\n                    onMainDispatcher {\n                        snackbar(getString(R.string.action_export_msg)).show()\n                    }\n                } catch (e: Exception) {\n                    Logs.w(e)\n                    onMainDispatcher {\n                        snackbar(e.readableMessage).show()\n                    }\n                }\n\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.view.View\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.databinding.LayoutDebugBinding\n\nclass DebugFragment : NamedFragment(R.layout.layout_debug) {\n\n    override fun name() = \"Debug\"\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val binding = LayoutDebugBinding.bind(view)\n\n        binding.debugCrash.setOnClickListener {\n            error(\"test crash\")\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.text.format.Formatter\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.LinearLayout\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.view.*\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.nekohasekai.sagernet.GroupType\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.GroupManager\nimport io.nekohasekai.sagernet.database.ProxyGroup\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.databinding.LayoutGroupItemBinding\nimport io.nekohasekai.sagernet.fmt.toUniversalLink\nimport io.nekohasekai.sagernet.group.GroupUpdater\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.widget.ListHolderListener\nimport io.nekohasekai.sagernet.widget.QRCodeDialog\nimport io.nekohasekai.sagernet.widget.UndoSnackbarManager\nimport kotlinx.coroutines.delay\nimport java.util.*\n\nclass GroupFragment : ToolbarFragment(R.layout.layout_group),\n    Toolbar.OnMenuItemClickListener {\n\n    lateinit var activity: MainActivity\n    lateinit var groupListView: RecyclerView\n    lateinit var layoutManager: LinearLayoutManager\n    lateinit var groupAdapter: GroupAdapter\n    lateinit var undoManager: UndoSnackbarManager<ProxyGroup>\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        activity = requireActivity() as MainActivity\n\n        ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)\n        toolbar.setTitle(R.string.menu_group)\n        toolbar.inflateMenu(R.menu.add_group_menu)\n        toolbar.setOnMenuItemClickListener(this)\n\n        groupListView = view.findViewById(R.id.group_list)\n        layoutManager = FixedLinearLayoutManager(groupListView)\n        groupListView.layoutManager = layoutManager\n        groupAdapter = GroupAdapter()\n        GroupManager.addListener(groupAdapter)\n        groupListView.adapter = groupAdapter\n\n        undoManager = UndoSnackbarManager(activity, groupAdapter)\n\n        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(\n            ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START\n        ) {\n            override fun getSwipeDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ): Int {\n                val proxyGroup = (viewHolder as GroupHolder).proxyGroup\n                if (proxyGroup.ungrouped || proxyGroup.id in GroupUpdater.updating) {\n                    return 0\n                }\n                return super.getSwipeDirs(recyclerView, viewHolder)\n            }\n\n            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n                val index = viewHolder.bindingAdapterPosition\n                groupAdapter.remove(index)\n                undoManager.remove(index to (viewHolder as GroupHolder).proxyGroup)\n            }\n\n            override fun onMove(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,\n            ): Boolean {\n                groupAdapter.move(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)\n                return true\n            }\n\n            override fun clearView(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) {\n                super.clearView(recyclerView, viewHolder)\n                groupAdapter.commitMove()\n            }\n        }).attachToRecyclerView(groupListView)\n\n    }\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_new_group -> {\n                startActivity(Intent(context, GroupSettingsActivity::class.java))\n            }\n        }\n        return true\n    }\n\n    private lateinit var selectedGroup: ProxyGroup\n\n    private val exportProfiles =\n        registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->\n            if (data != null) {\n                runOnDefaultDispatcher {\n                    val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id)\n                    val links = profiles.mapNotNull { it.toLink() }.joinToString(\"\\n\")\n                    try {\n                        (requireActivity() as MainActivity).contentResolver.openOutputStream(\n                            data\n                        )!!.bufferedWriter().use {\n                            it.write(links)\n                        }\n                        onMainDispatcher {\n                            snackbar(getString(R.string.action_export_msg)).show()\n                        }\n                    } catch (e: Exception) {\n                        Logs.w(e)\n                        onMainDispatcher {\n                            snackbar(e.readableMessage).show()\n                        }\n                    }\n\n                }\n            }\n        }\n\n    inner class GroupAdapter : RecyclerView.Adapter<GroupHolder>(),\n        GroupManager.Listener,\n        UndoSnackbarManager.Interface<ProxyGroup> {\n\n        val groupList = ArrayList<ProxyGroup>()\n\n        suspend fun reload() {\n            val groups = SagerDatabase.groupDao.allGroups().toMutableList()\n            val hideUngrouped =\n                SagerDatabase.proxyDao.countByGroup(groups.find { it.ungrouped }!!.id) == 0L\n            if (groups.size > 1 && hideUngrouped) groups.removeAll { it.ungrouped }\n\n            groupList.clear()\n            groupList.addAll(groups)\n            groupListView.post {\n                notifyDataSetChanged()\n            }\n        }\n\n        init {\n            setHasStableIds(true)\n\n            runOnDefaultDispatcher {\n                reload()\n            }\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GroupHolder {\n            return GroupHolder(LayoutGroupItemBinding.inflate(layoutInflater, parent, false))\n        }\n\n        override fun onBindViewHolder(holder: GroupHolder, position: Int) {\n            holder.bind(groupList[position])\n        }\n\n        override fun getItemCount(): Int {\n            return groupList.size\n        }\n\n        override fun getItemId(position: Int): Long {\n            return groupList[position].id\n        }\n\n        private val updated = HashSet<ProxyGroup>()\n\n        fun move(from: Int, to: Int) {\n            val first = groupList[from]\n            var previousOrder = first.userOrder\n            val (step, range) = if (from < to) Pair(1, from until to) else Pair(\n                -1, to + 1 downTo from\n            )\n            for (i in range) {\n                val next = groupList[i + step]\n                val order = next.userOrder\n                next.userOrder = previousOrder\n                previousOrder = order\n                groupList[i] = next\n                updated.add(next)\n            }\n            first.userOrder = previousOrder\n            groupList[to] = first\n            updated.add(first)\n            notifyItemMoved(from, to)\n        }\n\n        fun commitMove() = runOnDefaultDispatcher {\n            updated.forEach { SagerDatabase.groupDao.updateGroup(it) }\n            updated.clear()\n        }\n\n        fun remove(index: Int) {\n            groupList.removeAt(index)\n            notifyItemRemoved(index)\n        }\n\n        override fun undo(actions: List<Pair<Int, ProxyGroup>>) {\n            for ((index, item) in actions) {\n                groupList.add(index, item)\n                notifyItemInserted(index)\n            }\n        }\n\n        override fun commit(actions: List<Pair<Int, ProxyGroup>>) {\n            val groups = actions.map { it.second }\n            runOnDefaultDispatcher {\n                GroupManager.deleteGroup(groups)\n                reload()\n            }\n        }\n\n        override suspend fun groupAdd(group: ProxyGroup) {\n            if (groupList.size == 1 && groupList[0].ungrouped) {\n                groupList.clear()\n                onMainDispatcher {\n                    notifyItemRemoved(0)\n                }\n            }\n\n            groupList.add(group)\n            delay(300L)\n\n            onMainDispatcher {\n                undoManager.flush()\n                notifyItemInserted(groupList.size - 1)\n\n                if (group.type == GroupType.SUBSCRIPTION) {\n                    GroupUpdater.startUpdate(group, true)\n                }\n            }\n        }\n\n        override suspend fun groupRemoved(groupId: Long) {\n            val index = groupList.indexOfFirst { it.id == groupId }\n            if (index == -1) return\n            onMainDispatcher {\n                undoManager.flush()\n\n                groupList.removeAt(index)\n                notifyItemRemoved(index)\n            }\n        }\n\n\n        override suspend fun groupUpdated(group: ProxyGroup) {\n            val index = groupList.indexOfFirst { it.id == group.id }\n            if (index == -1) {\n                reload()\n                return\n            }\n            groupList[index] = group\n            onMainDispatcher {\n                undoManager.flush()\n\n                notifyItemChanged(index)\n            }\n        }\n\n        override suspend fun groupUpdated(groupId: Long) {\n            val index = groupList.indexOfFirst { it.id == groupId }\n            if (index == -1) {\n                reload()\n                return\n            }\n            onMainDispatcher {\n                notifyItemChanged(index)\n            }\n        }\n\n    }\n\n    override fun onDestroy() {\n        if (::groupAdapter.isInitialized) {\n            GroupManager.removeListener(groupAdapter)\n        }\n\n        super.onDestroy()\n\n        if (!::undoManager.isInitialized) return\n        undoManager.flush()\n    }\n\n\n    inner class GroupHolder(binding: LayoutGroupItemBinding) : RecyclerView.ViewHolder(binding.root),\n        PopupMenu.OnMenuItemClickListener {\n\n        lateinit var proxyGroup: ProxyGroup\n        val groupName = binding.groupName\n        val groupStatus = binding.groupStatus\n        val groupTraffic = binding.groupTraffic\n        val groupUser = binding.groupUser\n        val editButton = binding.edit\n        val optionsButton = binding.options\n        val updateButton = binding.groupUpdate\n        val subscriptionUpdateProgress = binding.subscriptionUpdateProgress\n\n        override fun onMenuItemClick(item: MenuItem): Boolean {\n            fun showCode(link: String) {\n                QRCodeDialog(link).showAllowingStateLoss(parentFragmentManager)\n            }\n\n            fun export(link: String) {\n                val success = SagerNet.trySetPrimaryClip(link)\n                (activity as MainActivity).snackbar(if (success) R.string.action_export_msg else R.string.action_export_err)\n                        .show()\n            }\n\n            when (item.itemId) {\n                R.id.action_universal_qr -> {\n                    showCode(proxyGroup.toUniversalLink())\n                }\n                R.id.action_universal_clipboard -> {\n                    export(proxyGroup.toUniversalLink())\n                }\n                R.id.action_export_clipboard -> {\n                    runOnDefaultDispatcher {\n                        val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id)\n                        val links = profiles.mapNotNull { it.toLink() }.joinToString(\"\\n\")\n                        onMainDispatcher {\n                            SagerNet.trySetPrimaryClip(links)\n                            snackbar(getString(R.string.copy_toast_msg)).show()\n                        }\n                    }\n                }\n                R.id.action_export_file -> {\n                    startFilesForResult(exportProfiles, \"profiles.txt\")\n                }\n                R.id.action_clear -> {\n                    MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm)\n                            .setMessage(R.string.clear_profiles_message)\n                            .setPositiveButton(R.string.yes) { _, _ ->\n                                runOnDefaultDispatcher {\n                                    GroupManager.clearGroup(proxyGroup.id)\n                                }\n                            }\n                            .setNegativeButton(android.R.string.cancel, null)\n                            .show()\n                }\n            }\n\n            return true\n        }\n\n\n        fun bind(group: ProxyGroup) {\n            proxyGroup = group\n\n            itemView.setOnClickListener { }\n\n            editButton.isGone = proxyGroup.ungrouped\n            updateButton.isInvisible = proxyGroup.type != GroupType.SUBSCRIPTION\n            groupName.text = proxyGroup.displayName()\n\n            editButton.setOnClickListener {\n                startActivity(Intent(it.context, GroupSettingsActivity::class.java).apply {\n                    putExtra(GroupSettingsActivity.EXTRA_GROUP_ID, group.id)\n                })\n            }\n\n            updateButton.setOnClickListener {\n                GroupUpdater.startUpdate(proxyGroup, true)\n            }\n\n            runOnDefaultDispatcher {\n                val showMenu = SagerDatabase.proxyDao.countByGroup(group.id) > 0\n                onMainDispatcher {\n                    optionsButton.isVisible = showMenu\n                }\n            }\n            optionsButton.setOnClickListener {\n                selectedGroup = proxyGroup\n\n                val popup = PopupMenu(requireContext(), it)\n                popup.menuInflater.inflate(R.menu.group_action_menu, popup.menu)\n\n                if (proxyGroup.type != GroupType.SUBSCRIPTION) {\n                    popup.menu.removeItem(R.id.action_share)\n                }\n                popup.setOnMenuItemClickListener(this)\n                popup.show()\n            }\n\n            if (proxyGroup.id in GroupUpdater.updating) {\n                (groupName.parent as LinearLayout).apply {\n                    setPadding(paddingLeft, dp2px(11), paddingRight, paddingBottom)\n                }\n\n                subscriptionUpdateProgress.isVisible = true\n\n                if (!GroupUpdater.progress.containsKey(proxyGroup.id)) {\n                    subscriptionUpdateProgress.isIndeterminate = true\n                } else {\n                    subscriptionUpdateProgress.isIndeterminate = false\n                    val progress = GroupUpdater.progress[proxyGroup.id]!!\n                    subscriptionUpdateProgress.max = progress.max\n                    subscriptionUpdateProgress.progress = progress.progress\n                }\n\n                updateButton.isInvisible = true\n                editButton.isGone = true\n            } else {\n                (groupName.parent as LinearLayout).apply {\n                    setPadding(paddingLeft, dp2px(15), paddingRight, paddingBottom)\n                }\n\n                subscriptionUpdateProgress.isVisible = false\n                updateButton.isInvisible = proxyGroup.type != GroupType.SUBSCRIPTION\n                editButton.isGone = proxyGroup.ungrouped\n            }\n\n            val subscription = proxyGroup.subscription\n            val textLayout = groupTraffic.parent as View\n            if (subscription != null && subscription.bytesUsed > 0L) {\n                groupTraffic.isVisible = true\n                groupTraffic.text = if (subscription.bytesRemaining > 0L) {\n                    getString(\n                        R.string.subscription_traffic, Formatter.formatFileSize(\n                            context, subscription.bytesUsed\n                        ), Formatter.formatFileSize(\n                            context, subscription.bytesRemaining\n                        )\n                    )\n                } else {\n                    getString(\n                        R.string.subscription_used, Formatter.formatFileSize(\n                            context, subscription.bytesUsed\n                        )\n                    )\n                }\n                groupStatus.setPadding(0)\n            } else {\n                groupTraffic.isVisible = false\n                groupStatus.setPadding(0, 0, 0, dp2px(4))\n            }\n\n            groupUser.text = subscription?.username ?: \"\"\n\n            runOnDefaultDispatcher {\n                val size = SagerDatabase.proxyDao.countByGroup(group.id)\n                onMainDispatcher {\n                    @Suppress(\"DEPRECATION\") when (group.type) {\n                        GroupType.BASIC -> {\n                            if (size == 0L) {\n                                groupStatus.setText(R.string.group_status_empty)\n                            } else {\n                                groupStatus.text = getString(R.string.group_status_proxies, size)\n                            }\n                        }\n                        GroupType.SUBSCRIPTION -> {\n                            groupStatus.text = if (size == 0L) {\n                                getString(R.string.group_status_empty_subscription)\n                            } else {\n                                val date = Date(group.subscription!!.lastUpdated * 1000L)\n                                getString(\n                                    R.string.group_status_proxies_subscription,\n                                    size,\n                                    \"${date.month + 1} - ${date.date}\"\n                                )\n                            }\n\n                        }\n                    }\n                }\n\n            }\n\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport androidx.annotation.LayoutRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.view.ViewCompat\nimport androidx.preference.*\nimport cn.hutool.core.util.NumberUtil\nimport com.github.shadowsocks.plugin.Empty\nimport com.github.shadowsocks.plugin.fragment.AlertDialogFragment\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.GroupType\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SubscriptionType\nimport io.nekohasekai.sagernet.database.*\nimport io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.utils.DirectBoot\nimport io.nekohasekai.sagernet.widget.ListListener\nimport io.nekohasekai.sagernet.widget.UserAgentPreference\nimport kotlinx.parcelize.Parcelize\n\n@Suppress(\"UNCHECKED_CAST\")\nclass GroupSettingsActivity(\n    @LayoutRes resId: Int = R.layout.layout_config_settings,\n) : ThemedActivity(resId),\n    OnPreferenceDataStoreChangeListener {\n\n    fun ProxyGroup.init() {\n        DataStore.groupName = name ?: \"\"\n        DataStore.groupType = type\n        DataStore.groupOrder = order\n        val subscription = subscription ?: SubscriptionBean().applyDefaultValues()\n        DataStore.subscriptionType = subscription.type\n        DataStore.subscriptionLink = subscription.link\n        DataStore.subscriptionToken = subscription.token\n        DataStore.subscriptionForceResolve = subscription.forceResolve\n        DataStore.subscriptionDeduplication = subscription.deduplication\n        DataStore.subscriptionForceVMessAEAD = subscription.forceVMessAEAD\n        DataStore.subscriptionUpdateWhenConnectedOnly = subscription.updateWhenConnectedOnly\n        DataStore.subscriptionUserAgent = subscription.customUserAgent\n        DataStore.subscriptionAutoUpdate = subscription.autoUpdate\n        DataStore.subscriptionAutoUpdateDelay = subscription.autoUpdateDelay\n    }\n\n    fun ProxyGroup.serialize() {\n        name = DataStore.groupName.takeIf { it.isNotBlank() }\n            ?: \"My group \" + System.currentTimeMillis() / 1000\n        type = DataStore.groupType\n        order = DataStore.groupOrder\n\n        val isSubscription = type == GroupType.SUBSCRIPTION\n        if (isSubscription) {\n            subscription = (subscription ?: SubscriptionBean().applyDefaultValues()).apply {\n                type = DataStore.subscriptionType\n                link = DataStore.subscriptionLink\n                token = DataStore.subscriptionToken\n                forceResolve = DataStore.subscriptionForceResolve\n                deduplication = DataStore.subscriptionDeduplication\n                forceVMessAEAD = DataStore.subscriptionForceVMessAEAD\n                updateWhenConnectedOnly = DataStore.subscriptionUpdateWhenConnectedOnly\n                customUserAgent = DataStore.subscriptionUserAgent\n                autoUpdate = DataStore.subscriptionAutoUpdate\n                autoUpdateDelay = DataStore.subscriptionAutoUpdateDelay\n            }\n        }\n    }\n\n    fun needSave(): Boolean {\n        if (!DataStore.dirty) return false\n        return true\n    }\n\n    fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.group_preferences)\n\n        val groupType = findPreference<SimpleMenuPreference>(Key.GROUP_TYPE)!!\n        val groupSubscription = findPreference<PreferenceCategory>(Key.GROUP_SUBSCRIPTION)!!\n        val subscriptionUpdate = findPreference<PreferenceCategory>(Key.SUBSCRIPTION_UPDATE)!!\n\n        fun updateGroupType(groupType: Int = DataStore.groupType) {\n            val isSubscription = groupType == GroupType.SUBSCRIPTION\n            groupSubscription.isVisible = isSubscription\n            subscriptionUpdate.isVisible = isSubscription\n        }\n        updateGroupType()\n        groupType.setOnPreferenceChangeListener { _, newValue ->\n            updateGroupType((newValue as String).toInt())\n            true\n        }\n\n        val subscriptionType = findPreference<SimpleMenuPreference>(Key.SUBSCRIPTION_TYPE)!!\n        val subscriptionLink = findPreference<EditTextPreference>(Key.SUBSCRIPTION_LINK)!!\n        val subscriptionToken = findPreference<EditTextPreference>(Key.SUBSCRIPTION_TOKEN)!!\n        val subscriptionForceVMessAEAD = findPreference<SwitchPreference>(Key.SUBSCRIPTION_FORCE_VMESS_AEAD)!!\n        val subscriptionUserAgent = findPreference<UserAgentPreference>(Key.SUBSCRIPTION_USER_AGENT)!!\n\n        fun updateSubscriptionType(subscriptionType: Int = DataStore.subscriptionType) {\n            val isRaw = subscriptionType == SubscriptionType.RAW\n            val isOOCv1 = subscriptionType == SubscriptionType.OOCv1\n\n            subscriptionForceVMessAEAD.isVisible = isRaw\n            subscriptionLink.isVisible = !isOOCv1\n            subscriptionToken.isVisible = isOOCv1\n            subscriptionUserAgent.isOOCv1 = isOOCv1\n            subscriptionUserAgent.notifyChanged()\n        }\n        updateSubscriptionType()\n        subscriptionType.setOnPreferenceChangeListener { _, newValue ->\n            updateSubscriptionType((newValue as String).toInt())\n            true\n        }\n\n        val subscriptionAutoUpdate = findPreference<SwitchPreference>(Key.SUBSCRIPTION_AUTO_UPDATE)!!\n        val subscriptionAutoUpdateDelay = findPreference<EditTextPreference>(Key.SUBSCRIPTION_AUTO_UPDATE_DELAY)!!\n        subscriptionAutoUpdateDelay.isEnabled = subscriptionAutoUpdate.isChecked\n        subscriptionAutoUpdateDelay.setOnPreferenceChangeListener { _, newValue ->\n            NumberUtil.isInteger(newValue as String) && newValue.toInt() >= 15\n        }\n        subscriptionAutoUpdate.setOnPreferenceChangeListener { _, newValue ->\n            subscriptionAutoUpdateDelay.isEnabled = (newValue as Boolean)\n            true\n        }\n    }\n\n    fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n    }\n\n    fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {\n        return false\n    }\n\n    class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.unsaved_changes_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                runOnDefaultDispatcher {\n                    (requireActivity() as GroupSettingsActivity).saveAndExit()\n                }\n            }\n            setNegativeButton(R.string.no) { _, _ ->\n                requireActivity().finish()\n            }\n            setNeutralButton(android.R.string.cancel, null)\n        }\n    }\n\n    @Parcelize\n    data class GroupIdArg(val groupId: Long) : Parcelable\n    class DeleteConfirmationDialogFragment : AlertDialogFragment<GroupIdArg, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.delete_group_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                runOnDefaultDispatcher {\n                    GroupManager.deleteGroup(arg.groupId)\n                }\n                requireActivity().finish()\n            }\n            setNegativeButton(R.string.no, null)\n        }\n    }\n\n    companion object {\n        const val EXTRA_GROUP_ID = \"id\"\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setTitle(R.string.group_settings)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        if (savedInstanceState == null) {\n            val editingId = intent.getLongExtra(EXTRA_GROUP_ID, 0L)\n            DataStore.editingId = editingId\n            runOnDefaultDispatcher {\n                if (editingId == 0L) {\n                    ProxyGroup().init()\n                } else {\n                    val entity = SagerDatabase.groupDao.getById(editingId)\n                    if (entity == null) {\n                        onMainDispatcher {\n                            finish()\n                        }\n                        return@runOnDefaultDispatcher\n                    }\n                    entity.init()\n                }\n\n                onMainDispatcher {\n                    supportFragmentManager.beginTransaction()\n                        .replace(R.id.settings, MyPreferenceFragmentCompat().apply {\n                            activity = this@GroupSettingsActivity\n                        })\n                        .commit()\n\n                    DataStore.dirty = false\n                    DataStore.profileCacheStore.registerChangeListener(this@GroupSettingsActivity)\n                }\n            }\n\n        }\n\n    }\n\n    suspend fun saveAndExit() {\n\n        val editingId = DataStore.editingId\n        if (editingId == 0L) {\n            GroupManager.createGroup(ProxyGroup().apply { serialize() })\n        } else if (needSave()) {\n            val entity = SagerDatabase.groupDao.getById(DataStore.editingId)\n            if (entity == null) {\n                finish()\n                return\n            }\n            GroupManager.updateGroup(entity.apply { serialize() })\n        }\n\n        if (editingId == DataStore.selectedProxy && DataStore.directBootAware) DirectBoot.update()\n        finish()\n\n    }\n\n    val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat }\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.profile_config_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)\n\n    override fun onBackPressed() {\n        if (needSave()) {\n            UnsavedChangesDialogFragment().apply { key() }.show(supportFragmentManager, null)\n        } else super.onBackPressed()\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        if (!super.onSupportNavigateUp()) finish()\n        return true\n    }\n\n    override fun onDestroy() {\n        DataStore.profileCacheStore.unregisterChangeListener(this)\n        super.onDestroy()\n    }\n\n    override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {\n        if (key != Key.PROFILE_DIRTY) {\n            DataStore.dirty = true\n        }\n    }\n\n    class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {\n\n        lateinit var activity: GroupSettingsActivity\n\n        override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {\n            preferenceManager.preferenceDataStore = DataStore.profileCacheStore\n            activity.apply {\n                createPreferences(savedInstanceState, rootKey)\n            }\n        }\n\n        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n            super.onViewCreated(view, savedInstanceState)\n\n            ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)\n\n            activity.apply {\n                viewCreated(view, savedInstanceState)\n            }\n        }\n\n        override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {\n            R.id.action_delete -> {\n                if (DataStore.editingId == 0L) {\n                    requireActivity().finish()\n                } else {\n                    DeleteConfirmationDialogFragment().apply {\n                        arg(GroupIdArg(DataStore.editingId))\n                        key()\n                    }.show(parentFragmentManager, null)\n                }\n                true\n            }\n            R.id.action_apply -> {\n                runOnDefaultDispatcher {\n                    activity.saveAndExit()\n                }\n                true\n            }\n            else -> false\n        }\n\n        override fun onDisplayPreferenceDialog(preference: Preference) {\n            activity.apply {\n                if (displayPreferenceDialog(preference)) return\n            }\n            super.onDisplayPreferenceDialog(preference)\n        }\n\n    }\n\n    object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {\n\n        override fun provideSummary(preference: EditTextPreference): CharSequence {\n            return if (preference.text.isNullOrBlank()) {\n                preference.context.getString(androidx.preference.R.string.not_set)\n            } else {\n                \"\\u2022\".repeat(preference.text.length)\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/LicenseActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport com.mikepenz.aboutlibraries.LibsBuilder\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.widget.ListHolderListener\n\nclass LicenseActivity : ThemedActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContentView(R.layout.layout_license)\n\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setTitle(R.string.oss_licenses)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n        ListHolderListener.setup(this)\n\n        val libs =\n            LibsBuilder().withExcludedLibraries( // Can't parse ${project.artifactId} in pom.xml\n                \"cn_hutool__hutool_core\", \"cn_hutool__hutool_json\", \"cn_hutool__hutool_crypto\", \"cn_hutool__hutool_cache\"\n            ).withAboutIconShown(false).withFields(R.string::class.java.fields).supportFragment()\n\n        supportFragmentManager.beginTransaction().replace(R.id.fragment_holder, libs)\n            .commitAllowingStateLoss()\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        finish()\n        return true\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.graphics.Typeface\nimport android.os.Bundle\nimport android.view.*\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.content.FileProvider\nimport androidx.core.graphics.TypefaceCompat\nimport androidx.drawerlayout.widget.DrawerLayout\nimport cn.hutool.core.util.RuntimeUtil\nimport com.termux.terminal.TerminalColors\nimport com.termux.terminal.TerminalSession\nimport com.termux.terminal.TerminalSessionClient\nimport com.termux.view.TerminalViewClient\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.databinding.LayoutLogcatBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.utils.CrashHandler\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.util.*\nimport kotlin.math.max\nimport kotlin.math.min\n\nclass LogcatFragment : ToolbarFragment(R.layout.layout_logcat),\n    TerminalSessionClient,\n    TerminalViewClient,\n    Toolbar.OnMenuItemClickListener {\n\n    lateinit var binding: LayoutLogcatBinding\n    var fontSize = dp2px(8)\n\n    @SuppressLint(\"RestrictedApi\")\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        toolbar.setTitle(R.string.menu_log)\n\n        toolbar.inflateMenu(R.menu.logcat_menu)\n        toolbar.setOnMenuItemClickListener(this)\n\n        binding = LayoutLogcatBinding.bind(view)\n        val terminalView = binding.terminalView\n\n        // Make it divisible by 2 since that is the minimal adjustment step:\n        if (fontSize % 2 == 1) fontSize--\n\n        terminalView.setTerminalViewClient(this)\n        terminalView.setTextSize(fontSize)\n        terminalView.setTypeface(\n            TypefaceCompat.createFromResourcesFontFile(\n                view.context,\n                resources,\n                R.font.jetbrains_mono,\n                \"res/font/jetbrains_mono.ttf\",\n                Typeface.MONOSPACE.style,\n            )\n        )\n\n        reloadSession()\n\n        registerForContextMenu(terminalView)\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {\n        binding.terminalView.showContextMenu()\n    }\n\n    fun reloadSession() {\n        val terminalView = binding.terminalView\n        terminalView.mTermSession?.also {\n            it.finishIfRunning()\n        }\n        val session = TerminalSession(\n            \"/system/bin/logcat\", app.cacheDir.absolutePath, arrayOf(\n                \"-C\",\n                \"-v\",\n                \"tag,color\",\n                \"AndroidRuntime:D\",\n                \"ProxyInstance:D\",\n                \"GuardedProcessPool:D\",\n                \"VpnService:D\",\n                \"libcore:D\",\n                \"xray-core:D\",\n\n                \"libsslocal:D\",\n                \"libss-local:D\",\n                \"libtrojan:D\",\n                \"libtrojan:D\",\n                \"libnaive:D\",\n                \"libbrook:D\",\n                \"libhysteria:D\",\n                \"libpingtunnel:D\",\n                \"librelaybaton:D\",\n                \"libwg:D\",\n\n                \"*:S\"\n            ), arrayOf(), 3000, this\n        )\n        terminalView.attachSession(session)\n        terminalView.updateSize()\n    }\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_clear_logcat -> {\n                runOnDefaultDispatcher {\n                    try {\n                        RuntimeUtil.exec(\"/system/bin/logcat\", \"-c\").waitFor()\n                    } catch (e: Exception) {\n                        onMainDispatcher {\n                            snackbar(e.readableMessage).show()\n                        }\n                        return@runOnDefaultDispatcher\n                    }\n                    onMainDispatcher {\n                        reloadSession()\n                    }\n                }\n\n            }\n            R.id.action_send_logcat -> {\n                val context = requireContext()\n\n                runOnDefaultDispatcher {\n                    val logFile = File.createTempFile(\"AnXray \",\n                        \".log\",\n                        File(app.cacheDir, \"log\").also { it.mkdirs() })\n\n                    var report = CrashHandler.buildReportHeader()\n\n                    report += \"Logcat: \\n\\n\"\n\n                    logFile.writeText(report)\n\n                    try {\n                        Runtime.getRuntime().exec(arrayOf(\"logcat\", \"-d\")).inputStream.use(\n                            FileOutputStream(\n                                logFile, true\n                            )\n                        )\n                    } catch (e: IOException) {\n                        Logs.w(e)\n                        logFile.appendText(\"Export logcat error: \" + CrashHandler.formatThrowable(e))\n                    }\n\n                    startActivity(\n                        Intent.createChooser(\n                            Intent(Intent.ACTION_SEND).setType(\"text/x-log\")\n                                .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                                .putExtra(\n                                    Intent.EXTRA_STREAM, FileProvider.getUriForFile(\n                                        context, BuildConfig.APPLICATION_ID + \".log\", logFile\n                                    )\n                                ), context.getString(R.string.abc_shareactionprovider_share_with)\n                        )\n                    )\n                }\n            }\n        }\n        return true\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n\n        if (::binding.isInitialized) {\n            binding.terminalView.mTermSession?.finishIfRunning()\n        }\n    }\n\n    override fun onTextChanged(changedSession: TerminalSession?) {\n    }\n\n    override fun onTitleChanged(changedSession: TerminalSession?) {\n    }\n\n    override fun onSessionFinished(finishedSession: TerminalSession?) {\n    }\n\n    override fun onCopyTextToClipboard(session: TerminalSession?, text: String?) {\n        if (text.isNullOrBlank()) return\n        SagerNet.trySetPrimaryClip(text)\n        snackbar(R.string.copy_success).show()\n    }\n\n    override fun onPasteTextFromClipboard(session: TerminalSession) {\n    }\n\n    override fun onBell(session: TerminalSession?) {\n    }\n\n    override fun onColorsChanged(session: TerminalSession?) {\n    }\n\n    override fun onTerminalCursorStateChange(state: Boolean) {\n    }\n\n    override fun getTerminalCursorStyle(): Int {\n        return 0\n    }\n\n    override fun onScale(scale: Float): Float {\n        if (scale < 0.9f || scale > 1.1f) {\n            val increase = scale > 1f\n            changeFontSize(increase)\n            return 1.0f\n        }\n        return scale\n    }\n\n    companion object {\n        private val MIN_FONTSIZE = dp2px(4)\n        private val MAX_FONTSIZE = dp2px(12)\n    }\n\n    private fun changeFontSize(increase: Boolean) {\n        val terminalView = binding.terminalView\n        fontSize += if (increase) 1 else -1\n        fontSize = max(MIN_FONTSIZE, min(fontSize, MAX_FONTSIZE))\n        terminalView.setTextSize(fontSize)\n    }\n\n\n    override fun onSingleTapUp(e: MotionEvent?) {\n    }\n\n    override fun shouldBackButtonBeMappedToEscape(): Boolean {\n        return false\n    }\n\n    override fun shouldEnforceCharBasedInput(): Boolean {\n        return false\n    }\n\n    override fun shouldUseCtrlSpaceWorkaround(): Boolean {\n        return false\n    }\n\n    override fun isTerminalViewSelected(): Boolean {\n        return true\n    }\n\n    override fun copyModeChanged(copyMode: Boolean) {\n        val activity = requireActivity() as MainActivity\n        // Disable drawer while copying.\n        activity.binding.drawerLayout.setDrawerLockMode(if (copyMode) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED)\n    }\n\n    override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {\n        return false\n    }\n\n    override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean {\n        return false\n    }\n\n    override fun onLongPress(event: MotionEvent?): Boolean {\n        return false\n    }\n\n    override fun readControlKey(): Boolean {\n        return false\n    }\n\n    override fun readAltKey(): Boolean {\n        return false\n    }\n\n    override fun readShiftKey(): Boolean {\n        return false\n    }\n\n    override fun readFnKey(): Boolean {\n        return false\n    }\n\n    override fun onCodePoint(\n        codePoint: Int, ctrlDown: Boolean, session: TerminalSession?\n    ): Boolean {\n        return false\n    }\n\n    override fun disableInput(): Boolean {\n        return true\n    }\n\n    override fun onEmulatorSet() {\n        val props = Properties()\n        props.load(requireContext().assets.open(\"terminal.properties\"))\n        TerminalColors.COLOR_SCHEME.updateWith(props)\n        val emulator = binding.terminalView.mTermSession.emulator\n        emulator.mColors.reset()\n    }\n\n    override fun onScroll(offset: Int) {\n        val activity = requireActivity() as MainActivity\n        val topRow = binding.terminalView.topRow\n        if (offset < 0) {\n            activity.binding.stats.apply {\n                if (isShown) performHide()\n            }\n        }\n\n        val screen = binding.terminalView.mEmulator.screen\n\n        if (topRow == 0 && screen.activeTranscriptRows > 0) activity.binding.fab.apply {\n            if (isShown) hide()\n        } else activity.binding.fab.apply {\n            if (!isShown) show()\n        }\n    }\n\n    override fun logError(tag: String?, message: String?) {\n    }\n\n    override fun logWarn(tag: String?, message: String?) {\n    }\n\n    override fun logInfo(tag: String?, message: String?) {\n    }\n\n    override fun logDebug(tag: String?, message: String?) {\n    }\n\n    override fun logVerbose(tag: String?, message: String?) {\n    }\n\n    override fun logStackTraceWithMessage(tag: String?, message: String?, e: Exception?) {\n    }\n\n    override fun logStackTrace(tag: String?, e: Exception?) {\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.RemoteException\nimport android.provider.Settings\nimport android.view.KeyEvent\nimport android.view.MenuItem\nimport android.widget.Toast\nimport androidx.annotation.IdRes\nimport androidx.core.view.ViewCompat\nimport androidx.preference.PreferenceDataStore\nimport cn.hutool.core.codec.Base64Decoder\nimport cn.hutool.core.util.ZipUtil\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.navigation.NavigationView\nimport com.google.android.material.snackbar.Snackbar\nimport io.nekohasekai.sagernet.*\nimport io.nekohasekai.sagernet.aidl.AppStats\nimport io.nekohasekai.sagernet.aidl.ISagerNetService\nimport io.nekohasekai.sagernet.aidl.TrafficStats\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.bg.SagerConnection\nimport io.nekohasekai.sagernet.database.*\nimport io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener\nimport io.nekohasekai.sagernet.databinding.LayoutMainBinding\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.fmt.KryoConverters\nimport io.nekohasekai.sagernet.fmt.PluginEntry\nimport io.nekohasekai.sagernet.group.GroupInterfaceAdapter\nimport io.nekohasekai.sagernet.group.GroupUpdater\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.plugin.PluginManager\nimport io.nekohasekai.sagernet.widget.ListHolderListener\nimport com.github.shadowsocks.plugin.PluginManager as ShadowsocksPluginPluginManager\n\nclass MainActivity : ThemedActivity(),\n    SagerConnection.Callback,\n    OnPreferenceDataStoreChangeListener,\n    NavigationView.OnNavigationItemSelectedListener {\n\n    lateinit var binding: LayoutMainBinding\n    lateinit var navigation: NavigationView\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        binding = LayoutMainBinding.inflate(layoutInflater)\n        binding.fab.initProgress(binding.fabProgress)\n        if (themeResId !in intArrayOf(\n                R.style.Theme_SagerNet_Black, R.style.Theme_SagerNet_LightBlack\n            )\n        ) {\n            navigation = binding.navView\n            binding.drawerLayout.removeView(binding.navViewBlack)\n        } else {\n            navigation = binding.navViewBlack\n            binding.drawerLayout.removeView(binding.navView)\n        }\n        navigation.setNavigationItemSelectedListener(this)\n\n        if (savedInstanceState == null) {\n            displayFragmentWithId(R.id.nav_configuration)\n        }\n\n        binding.fab.setOnClickListener {\n            if (state.canStop) SagerNet.stopService() else connect.launch(\n                null\n            )\n        }\n        binding.stats.setOnClickListener { if (state == BaseService.State.Connected) binding.stats.testConnection() }\n\n        setContentView(binding.root)\n        ViewCompat.setOnApplyWindowInsetsListener(binding.coordinator, ListHolderListener)\n        changeState(BaseService.State.Idle)\n        connection.connect(this, this)\n        DataStore.configurationStore.registerChangeListener(this)\n        GroupManager.userInterface = GroupInterfaceAdapter(this)\n\n        if (intent?.action == Intent.ACTION_VIEW) {\n            onNewIntent(intent)\n        }\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n\n        val uri = intent.data ?: return\n\n        runOnDefaultDispatcher {\n            if (uri.scheme == \"sn\" && uri.host == \"subscription\" || uri.scheme == \"clash\") {\n                importSubscription(uri)\n            } else {\n                importProfile(uri)\n            }\n        }\n    }\n\n    fun urlTest(): Int {\n        if (state != BaseService.State.Connected || connection.service == null) {\n            error(\"not started\")\n        }\n        return connection.service!!.urlTest()\n    }\n\n    suspend fun importSubscription(uri: Uri) {\n        val group: ProxyGroup\n\n        val url = uri.getQueryParameter(\"url\")\n        if (!url.isNullOrBlank()) {\n            group = ProxyGroup(type = GroupType.SUBSCRIPTION)\n            val subscription = SubscriptionBean()\n            group.subscription = subscription\n\n            // cleartext format\n            subscription.link = url\n            group.name = uri.getQueryParameter(\"name\")\n\n            val type = uri.getQueryParameter(\"type\")\n            when (type?.lowercase()) {\n                \"sip008\" -> {\n                    subscription.type = SubscriptionType.SIP008\n                }\n            }\n\n        } else {\n            val data = uri.encodedQuery.takeIf { !it.isNullOrBlank() } ?: return\n            try {\n                group = KryoConverters.deserialize(\n                    ProxyGroup().apply { export = true }, ZipUtil.unZlib(Base64Decoder.decode(data))\n                ).apply {\n                    export = false\n                }\n            } catch (e: Exception) {\n                onMainDispatcher {\n                    alert(e.readableMessage).show()\n                }\n                return\n            }\n        }\n\n        val name = group.name.takeIf { !it.isNullOrBlank() } ?: group.subscription?.link\n        ?: group.subscription?.token\n        if (name.isNullOrBlank()) return\n\n        group.name = group.name.takeIf { !it.isNullOrBlank() }\n            ?: \"Subscription #\" + System.currentTimeMillis()\n\n        onMainDispatcher {\n\n            displayFragmentWithId(R.id.nav_group)\n\n            MaterialAlertDialogBuilder(this@MainActivity).setTitle(R.string.subscription_import)\n                .setMessage(getString(R.string.subscription_import_message, name))\n                .setPositiveButton(R.string.yes) { _, _ ->\n                    runOnDefaultDispatcher {\n                        finishImportSubscription(group)\n                    }\n                }\n                .setNegativeButton(android.R.string.cancel, null)\n                .show()\n\n        }\n\n    }\n\n    private suspend fun finishImportSubscription(subscription: ProxyGroup) {\n        GroupManager.createGroup(subscription)\n        GroupUpdater.startUpdate(subscription, true)\n    }\n\n    suspend fun importProfile(uri: Uri) {\n        val profile = try {\n            parseProxies(uri.toString()).getOrNull(0) ?: error(getString(R.string.no_proxies_found))\n        } catch (e: Exception) {\n            onMainDispatcher {\n                alert(e.readableMessage).show()\n            }\n            return\n        }\n\n        onMainDispatcher {\n            MaterialAlertDialogBuilder(this@MainActivity).setTitle(R.string.profile_import)\n                .setMessage(getString(R.string.profile_import_message, profile.displayName()))\n                .setPositiveButton(R.string.yes) { _, _ ->\n                    runOnDefaultDispatcher {\n                        finishImportProfile(profile)\n                    }\n                }\n                .setNegativeButton(android.R.string.cancel, null)\n                .show()\n        }\n\n    }\n\n    private suspend fun finishImportProfile(profile: AbstractBean) {\n        val targetId = DataStore.selectedGroupForImport()\n\n        ProfileManager.createProfile(targetId, profile)\n\n        onMainDispatcher {\n            displayFragmentWithId(R.id.nav_configuration)\n\n            snackbar(resources.getQuantityString(R.plurals.added, 1, 1)).show()\n        }\n    }\n\n    override fun missingPlugin(profileName: String, pluginName: String) {\n        val pluginId = if (pluginName.startsWith(\"shadowsocks-\")) pluginName.substringAfter(\"shadowsocks-\") else pluginName\n        val pluginEntity = PluginEntry.find(pluginName)\n        if (pluginEntity == null) {\n            snackbar(getString(R.string.plugin_unknown, pluginName)).show()\n            return\n        }\n\n        val existsButOnShitSystem = if (pluginName == pluginId) {\n            PluginManager.fetchPlugins().map { it.id }.contains(pluginName)\n        } else {\n            ShadowsocksPluginPluginManager.fetchPlugins(true).map { it.id }.contains(pluginId)\n        }\n\n        if (existsButOnShitSystem) {\n            MaterialAlertDialogBuilder(this).setTitle(R.string.missing_plugin).setMessage(\n                getString(\n                    R.string.plugin_exists_but_on_shit_system,\n                    profileName,\n                    getString(pluginEntity.nameId)\n                )\n            ).setPositiveButton(R.string.action_learn_more) { _, _ ->\n                launchCustomTab(\"https://sagernet.org/plugin/\")\n            }.show()\n            return\n        }\n\n        MaterialAlertDialogBuilder(this).setTitle(R.string.missing_plugin)\n            .setMessage(\n                getString(\n                    R.string.profile_requiring_plugin, profileName, getString(pluginEntity.nameId)\n                )\n            )\n            .setPositiveButton(R.string.action_download) { _, _ ->\n                showDownloadDialog(pluginEntity)\n            }\n            .setNeutralButton(android.R.string.cancel, null)\n            .setNeutralButton(R.string.action_learn_more) { _, _ ->\n                launchCustomTab(\"https://sagernet.org/plugin/\")\n            }\n            .show()\n    }\n\n    private fun showDownloadDialog(pluginEntry: PluginEntry) {\n        var index = 0\n        var playIndex = -1\n        var fdroidIndex = -1\n        var downloadIndex = -1\n\n        val items = mutableListOf<String>()\n        if (pluginEntry.downloadSource.playStore) {\n            items.add(getString(R.string.install_from_play_store))\n            playIndex = index++\n        }\n        if (pluginEntry.downloadSource.fdroid) {\n            items.add(getString(R.string.install_from_fdroid))\n            fdroidIndex = index++\n        }\n\n        items.add(getString(R.string.download))\n        downloadIndex = index\n\n        MaterialAlertDialogBuilder(this).setTitle(pluginEntry.name)\n            .setItems(items.toTypedArray()) { _, which ->\n                when (which) {\n                    playIndex -> launchCustomTab(\"https://play.google.com/store/apps/details?id=${pluginEntry.packageName}\")\n                    fdroidIndex -> launchCustomTab(\"https://f-droid.org/packages/${pluginEntry.packageName}/\")\n                    downloadIndex -> launchCustomTab(pluginEntry.downloadSource.downloadLink)\n                }\n            }\n            .show()\n    }\n\n    override fun onNavigationItemSelected(item: MenuItem): Boolean {\n        if (item.isChecked) binding.drawerLayout.closeDrawers() else {\n            return displayFragmentWithId(item.itemId)\n        }\n        return true\n    }\n\n\n    fun displayFragment(fragment: ToolbarFragment) {\n        supportFragmentManager.beginTransaction()\n            .replace(R.id.fragment_holder, fragment)\n            .commitAllowingStateLoss()\n        binding.drawerLayout.closeDrawers()\n    }\n\n    fun displayFragmentWithId(@IdRes id: Int): Boolean {\n        when (id) {\n            R.id.nav_configuration -> {\n                displayFragment(ConfigurationFragment())\n                connection.bandwidthTimeout = connection.bandwidthTimeout\n            }\n            R.id.nav_group -> displayFragment(GroupFragment())\n            R.id.nav_route -> displayFragment(RouteFragment())\n            R.id.nav_settings -> displayFragment(SettingsFragment())\n            R.id.nav_traffic -> {\n                displayFragment(TrafficFragment())\n                connection.trafficTimeout = connection.trafficTimeout\n            }\n            R.id.nav_tools -> displayFragment(ToolsFragment())\n            R.id.nav_logcat -> displayFragment(LogcatFragment())\n            R.id.nav_faq -> {\n                launchCustomTab(\"https://anxray.org/\")\n                return false\n            }\n            R.id.nav_about -> displayFragment(AboutFragment())\n            else -> return false\n        }\n        navigation.menu.findItem(id).isChecked = true\n        return true\n    }\n\n    fun ruleCreated() {\n        navigation.menu.findItem(R.id.nav_route).isChecked = true\n        supportFragmentManager.beginTransaction()\n            .replace(R.id.fragment_holder, RouteFragment())\n            .commitAllowingStateLoss()\n        if (SagerNet.started) {\n            snackbar(getString(R.string.restart)).setAction(R.string.apply) {\n                SagerNet.reloadService()\n            }.show()\n        }\n    }\n\n    var state = BaseService.State.Idle\n    var doStop = false\n\n    private fun changeState(\n        state: BaseService.State,\n        msg: String? = null,\n        animate: Boolean = false,\n    ) {\n        val started = state == BaseService.State.Connected\n\n        if (!started) {\n            statsUpdated(emptyList())\n        }\n\n        binding.fab.changeState(state, this.state, animate)\n        binding.stats.changeState(state)\n        if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()\n        this.state = state\n\n        when (state) {\n            BaseService.State.Connected, BaseService.State.Stopped -> {\n                statsUpdated(emptyList())\n            }\n        }\n    }\n\n    override fun snackbarInternal(text: CharSequence): Snackbar {\n        return Snackbar.make(binding.coordinator, text, Snackbar.LENGTH_LONG).apply {\n            if (binding.fab.isShown) {\n                anchorView = binding.fab\n            }\n        }\n    }\n\n    override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {\n        changeState(state, msg, true)\n    }\n\n    override fun statsUpdated(stats: List<AppStats>) {\n        (supportFragmentManager.findFragmentById(R.id.fragment_holder) as? TrafficFragment)?.emitStats(\n            stats\n        )\n    }\n\n    override fun routeAlert(type: Int, routeName: String) {\n        when (type) {\n            0 -> {\n                // need vpn\n\n                Toast.makeText(\n                    this, getString(R.string.route_need_vpn, routeName), Toast.LENGTH_SHORT\n                ).show()\n            }\n            1 -> {\n                // need fds\n\n                MaterialAlertDialogBuilder(this).setTitle(R.string.foreground_detector)\n                    .setMessage(getString(R.string.route_need_fds, routeName))\n                    .setPositiveButton(R.string.enable) { _, _ ->\n                        startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))\n                    }\n                    .setNegativeButton(android.R.string.cancel, null)\n                    .show()\n            }\n        }\n    }\n\n    val connection = SagerConnection(true)\n    override fun onServiceConnected(service: ISagerNetService) = changeState(\n        try {\n            BaseService.State.values()[service.state].also {\n                SagerNet.started = it.canStop\n            }\n        } catch (_: RemoteException) {\n            BaseService.State.Idle\n        }\n    )\n\n    override fun onServiceDisconnected() = changeState(BaseService.State.Idle)\n    override fun onBinderDied() {\n        connection.disconnect(this)\n        connection.connect(this, this)\n    }\n\n    private val connect = registerForActivityResult(VpnRequestActivity.StartService()) {\n        if (it) snackbar(R.string.vpn_permission_denied).show()\n    }\n\n    override fun trafficUpdated(profileId: Long, stats: TrafficStats, isCurrent: Boolean) {\n        if (profileId == 0L) return\n\n        if (isCurrent) binding.stats.updateTraffic(\n            stats.txRateProxy, stats.rxRateProxy\n        )\n\n        runOnDefaultDispatcher {\n            ProfileManager.postTrafficUpdated(profileId, stats)\n        }\n    }\n\n    override fun profilePersisted(profileId: Long) {\n        runOnDefaultDispatcher {\n            ProfileManager.postUpdate(profileId)\n        }\n    }\n\n    override fun observatoryResultsUpdated(groupId: Long) {\n        runOnDefaultDispatcher {\n            GroupManager.postReload(groupId)\n        }\n    }\n\n    override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {\n        when (key) {\n            Key.SERVICE_MODE -> onBinderDied()\n            Key.PROXY_APPS, Key.BYPASS_MODE, Key.INDIVIDUAL -> {\n                if (state.canStop) {\n                    snackbar(getString(R.string.restart)).setAction(R.string.apply) {\n                        SagerNet.reloadService()\n                    }.show()\n                }\n            }\n        }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        connection.bandwidthTimeout = 1000\n    }\n\n    override fun onStop() {\n        connection.bandwidthTimeout = 0\n        super.onStop()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        GroupManager.userInterface = null\n        DataStore.configurationStore.unregisterChangeListener(this)\n        connection.disconnect(this)\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        when (keyCode) {\n            KeyEvent.KEYCODE_DPAD_LEFT -> {\n                if (super.onKeyDown(keyCode, event)) return true\n                binding.drawerLayout.open()\n                navigation.requestFocus()\n            }\n            KeyEvent.KEYCODE_DPAD_RIGHT -> {\n                if (binding.drawerLayout.isOpen) {\n                    binding.drawerLayout.close()\n                    return true\n                }\n            }\n        }\n\n        if (super.onKeyDown(keyCode, event)) return true\n        if (binding.drawerLayout.isOpen) return false\n\n        val fragment = supportFragmentManager.findFragmentById(R.id.fragment_holder) as? ToolbarFragment\n        return fragment != null && fragment.onKeyDown(keyCode, event)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/NamedFragment.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport androidx.fragment.app.Fragment\n\nabstract class NamedFragment : Fragment {\n\n    constructor() : super()\n    constructor(contentLayoutId: Int) : super(contentLayoutId)\n\n    abstract fun name(): String\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ProfileSelectActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.Intent\nimport android.os.Bundle\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.ProxyEntity\n\nclass ProfileSelectActivity : ThemedActivity(R.layout.layout_empty) {\n\n    companion object {\n        const val EXTRA_SELECTED = \"selected\"\n        const val EXTRA_PROFILE_ID = \"id\"\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val selected = intent.getParcelableExtra<ProxyEntity>(EXTRA_SELECTED)\n\n        supportFragmentManager.beginTransaction()\n            .replace(R.id.fragment_holder, ConfigurationFragment(true, selected))\n            .commitAllowingStateLoss()\n    }\n\n    fun returnProfile(profileId: Long) {\n        setResult(RESULT_OK, Intent().apply {\n            putExtra(EXTRA_PROFILE_ID, profileId)\n        })\n        finish()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.view.ViewCompat\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.database.RuleEntity\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.databinding.LayoutEmptyRouteBinding\nimport io.nekohasekai.sagernet.databinding.LayoutRouteItemBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.widget.ListHolderListener\nimport io.nekohasekai.sagernet.widget.UndoSnackbarManager\n\nclass RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItemClickListener {\n\n    lateinit var activity: MainActivity\n    lateinit var ruleListView: RecyclerView\n    lateinit var ruleAdapter: RuleAdapter\n    lateinit var undoManager: UndoSnackbarManager<RuleEntity>\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        activity = requireActivity() as MainActivity\n\n        ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)\n        toolbar.setTitle(R.string.menu_route)\n        toolbar.inflateMenu(R.menu.add_route_menu)\n        toolbar.setOnMenuItemClickListener(this)\n\n        ruleListView = view.findViewById(R.id.route_list)\n        ruleListView.layoutManager = FixedLinearLayoutManager(ruleListView)\n        ruleAdapter = RuleAdapter()\n        ProfileManager.addListener(ruleAdapter)\n        ruleListView.adapter = ruleAdapter\n        undoManager = UndoSnackbarManager(activity, ruleAdapter)\n\n        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START) {\n\n            override fun getSwipeDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) = if (viewHolder is RuleAdapter.DocumentHolder) {\n                0\n            } else {\n                super.getSwipeDirs(recyclerView, viewHolder)\n            }\n\n            override fun getDragDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) = if (viewHolder is RuleAdapter.DocumentHolder) {\n                0\n            } else {\n                super.getDragDirs(recyclerView, viewHolder)\n            }\n\n            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n                val index = viewHolder.bindingAdapterPosition\n                ruleAdapter.remove(index)\n                undoManager.remove(index to (viewHolder as RuleAdapter.RuleHolder).rule)\n            }\n\n            override fun onMove(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,\n            ): Boolean {\n                return if (target is RuleAdapter.DocumentHolder) {\n                    false\n                } else {\n                    ruleAdapter.move(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)\n                    true\n                }\n            }\n\n            override fun clearView(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) {\n                super.clearView(recyclerView, viewHolder)\n                ruleAdapter.commitMove()\n            }\n        }).attachToRecyclerView(ruleListView)\n    }\n\n    override fun onDestroy() {\n        if (::ruleAdapter.isInitialized) {\n            ProfileManager.removeListener(ruleAdapter)\n        }\n        super.onDestroy()\n    }\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_new_route -> {\n                startActivity(Intent(context, RouteSettingsActivity::class.java))\n            }\n            R.id.action_reset_route -> {\n                runOnDefaultDispatcher {\n                    SagerDatabase.rulesDao.deleteAll()\n                    DataStore.rulesFirstCreate = false\n                    ruleAdapter.reload()\n                }\n            }\n            R.id.action_manage_assets -> {\n                startActivity(Intent(requireContext(), AssetsActivity::class.java))\n            }\n        }\n        return true\n    }\n\n    inner class RuleAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ProfileManager.RuleListener, UndoSnackbarManager.Interface<RuleEntity> {\n\n        val ruleList = ArrayList<RuleEntity>()\n        suspend fun reload() {\n            val rules = ProfileManager.getRules()\n            ruleListView.post {\n                ruleList.clear()\n                ruleList.addAll(rules)\n                ruleAdapter.notifyDataSetChanged()\n            }\n        }\n\n        init {\n            runOnDefaultDispatcher {\n                reload()\n            }\n        }\n\n        override fun onCreateViewHolder(\n            parent: ViewGroup,\n            viewType: Int,\n        ): RecyclerView.ViewHolder {\n            return if (viewType == 0) {\n                DocumentHolder(LayoutEmptyRouteBinding.inflate(layoutInflater, parent, false))\n            } else {\n                RuleHolder(LayoutRouteItemBinding.inflate(layoutInflater, parent, false))\n            }\n        }\n\n        override fun getItemViewType(position: Int): Int {\n            if (position == 0) return 0\n            return 1\n        }\n\n        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {\n            if (holder is DocumentHolder) {\n                holder.bind()\n            } else if (holder is RuleHolder) {\n                holder.bind(ruleList[position - 1])\n            }\n        }\n\n        override fun getItemCount(): Int {\n            return ruleList.size + 1\n        }\n\n        override fun getItemId(position: Int): Long {\n            if (position == 0) return 0L\n            return ruleList[position - 1].id\n        }\n\n        private val updated = HashSet<RuleEntity>()\n        fun move(from: Int, to: Int) {\n            val first = ruleList[from - 1]\n            var previousOrder = first.userOrder\n            val (step, range) = if (from < to) Pair(1, from - 1 until to - 1) else Pair(-1, to downTo from - 1)\n            for (i in range) {\n                val next = ruleList[i + step]\n                val order = next.userOrder\n                next.userOrder = previousOrder\n                previousOrder = order\n                ruleList[i] = next\n                updated.add(next)\n            }\n            first.userOrder = previousOrder\n            ruleList[to - 1] = first\n            updated.add(first)\n            notifyItemMoved(from, to)\n        }\n\n        fun commitMove() = runOnDefaultDispatcher {\n            if (updated.isNotEmpty()) {\n                SagerDatabase.rulesDao.updateRules(updated.toList())\n                updated.clear()\n                needReload()\n            }\n        }\n\n        fun remove(index: Int) {\n            ruleList.removeAt(index - 1)\n            notifyItemRemoved(index)\n        }\n\n        override fun undo(actions: List<Pair<Int, RuleEntity>>) {\n            for ((index, item) in actions) {\n                ruleList.add(index - 1, item)\n                notifyItemInserted(index)\n            }\n        }\n\n        override fun commit(actions: List<Pair<Int, RuleEntity>>) {\n            val rules = actions.map { it.second }\n            runOnDefaultDispatcher {\n                ProfileManager.deleteRules(rules)\n            }\n        }\n\n        override suspend fun onAdd(rule: RuleEntity) {\n            ruleListView.post {\n                ruleList.add(rule)\n                ruleAdapter.notifyItemInserted(ruleList.size)\n                needReload()\n            }\n        }\n\n        override suspend fun onUpdated(rule: RuleEntity) {\n            val index = ruleList.indexOfFirst { it.id == rule.id }\n            if (index == -1) return\n            ruleListView.post {\n                ruleList[index] = rule\n                ruleAdapter.notifyItemChanged(index + 1)\n                needReload()\n            }\n        }\n\n        override suspend fun onRemoved(ruleId: Long) {\n            val index = ruleList.indexOfFirst { it.id == ruleId }\n            if (index == -1) {\n                onMainDispatcher {\n                    needReload()\n                }\n            } else ruleListView.post {\n                ruleList.removeAt(index)\n                ruleAdapter.notifyItemRemoved(index + 1)\n                needReload()\n            }\n        }\n\n        override suspend fun onCleared() {\n            ruleListView.post {\n                ruleList.clear()\n                ruleAdapter.notifyDataSetChanged()\n                needReload()\n            }\n        }\n\n        inner class DocumentHolder(binding: LayoutEmptyRouteBinding) : RecyclerView.ViewHolder(binding.root) {\n            fun bind() {\n                itemView.setOnClickListener {\n                    it.context.launchCustomTab(\"https://www.v2fly.org/config/routing.html#ruleobject\")\n                }\n            }\n        }\n\n        inner class RuleHolder(binding: LayoutRouteItemBinding) : RecyclerView.ViewHolder(binding.root) {\n\n            lateinit var rule: RuleEntity\n            val profileName = binding.profileName\n            val profileType = binding.profileType\n            val routeOutbound = binding.routeOutbound\n            val editButton = binding.edit\n            val shareLayout = binding.share\n            val enableSwitch = binding.enable\n\n            fun bind(ruleEntity: RuleEntity) {\n                rule = ruleEntity\n                profileName.text = rule.displayName()\n                profileType.text = rule.mkSummary()\n                routeOutbound.text = rule.displayOutbound()\n                itemView.setOnClickListener {\n                    enableSwitch.performClick()\n                }\n                enableSwitch.isChecked = rule.enabled\n                enableSwitch.setOnCheckedChangeListener { _, isChecked ->\n                    runOnDefaultDispatcher {\n                        rule.enabled = isChecked\n                        SagerDatabase.rulesDao.updateRule(rule)\n                        onMainDispatcher {\n                            needReload()\n                        }\n                    }\n                }\n                editButton.setOnClickListener {\n                    startActivity(Intent(it.context, RouteSettingsActivity::class.java).apply {\n                        putExtra(RouteSettingsActivity.EXTRA_ROUTE_ID, rule.id)\n                    })\n                }\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport androidx.activity.result.component1\nimport androidx.activity.result.component2\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.annotation.LayoutRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.view.ViewCompat\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceDataStore\nimport androidx.preference.SwitchPreference\nimport com.github.shadowsocks.plugin.Empty\nimport com.github.shadowsocks.plugin.fragment.AlertDialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.AppStatus\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.database.RuleEntity\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.utils.DirectBoot\nimport io.nekohasekai.sagernet.utils.PackageCache\nimport io.nekohasekai.sagernet.widget.AppListPreference\nimport io.nekohasekai.sagernet.widget.ListListener\nimport io.nekohasekai.sagernet.widget.OutboundPreference\nimport kotlinx.parcelize.Parcelize\n\n@Suppress(\"UNCHECKED_CAST\")\nclass RouteSettingsActivity(\n    @LayoutRes resId: Int = R.layout.layout_settings_activity,\n) : ThemedActivity(resId),\n    OnPreferenceDataStoreChangeListener {\n\n    fun init(packageName: String?) {\n        RuleEntity().apply {\n            if (!packageName.isNullOrBlank()) {\n                packages = listOf(packageName)\n                name = app.getString(R.string.route_for, PackageCache.loadLabel(packageName))\n            }\n        }.init()\n    }\n\n    fun RuleEntity.init() {\n        DataStore.routeName = name\n        DataStore.routeDomain = domains\n        DataStore.routeIP = ip\n        DataStore.routePort = port\n        DataStore.routeSourcePort = sourcePort\n        DataStore.routeNetwork = network\n        DataStore.routeSource = source\n        DataStore.routeProtocol = protocol\n        DataStore.routeAttrs = attrs\n        DataStore.routeOutboundRule = outbound\n        DataStore.routeOutbound = when (outbound) {\n            0L -> 0\n            -1L -> 1\n            -2L -> 2\n            else -> 3\n        }\n        DataStore.routeReverse = reverse\n        DataStore.routeRedirect = redirect\n        DataStore.routePackages = packages.joinToString(\"\\n\")\n        DataStore.routeForegroundStatus = \"\"\n\n        for (status in appStatus) when (status) {\n            AppStatus.FOREGROUND, AppStatus.BACKGROUND -> {\n                DataStore.routeForegroundStatus = status\n            }\n        }\n    }\n\n    fun RuleEntity.serialize() {\n        name = DataStore.routeName\n        domains = DataStore.routeDomain\n        ip = DataStore.routeIP\n        port = DataStore.routePort\n        sourcePort = DataStore.routeSourcePort\n        network = DataStore.routeNetwork\n        source = DataStore.routeSource\n        protocol = DataStore.routeProtocol\n        attrs = DataStore.routeAttrs\n        outbound = when (DataStore.routeOutbound) {\n            0 -> 0L\n            1 -> -1L\n            2 -> -2L\n            else -> DataStore.routeOutboundRule\n        }\n        reverse = DataStore.routeReverse\n        redirect = DataStore.routeRedirect\n        packages = DataStore.routePackages.split(\"\\n\").filter { it.isNotBlank() }\n        val routeForegroundStatus = DataStore.routeForegroundStatus\n        appStatus = if (routeForegroundStatus.isNotBlank()) {\n            listOf(routeForegroundStatus)\n        } else {\n            emptyList()\n        }\n\n        if (DataStore.editingId == 0L) {\n            enabled = true\n        }\n    }\n\n    fun needSave(): Boolean {\n        if (!DataStore.dirty) return false\n        if (DataStore.routePackages.isBlank() && DataStore.routeForegroundStatus.isBlank() && DataStore.routeDomain.isBlank() && DataStore.routeIP.isBlank() && DataStore.routePort.isBlank() && DataStore.routeSourcePort.isBlank() && DataStore.routeNetwork.isBlank() && DataStore.routeSource.isBlank() && DataStore.routeProtocol.isBlank() && DataStore.routeAttrs.isBlank() && !(DataStore.routeReverse && DataStore.routeRedirect.isNotBlank())) {\n            return false\n        }\n        return true\n    }\n\n    fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.route_preferences)\n    }\n\n    val selectProfileForAdd = registerForActivityResult(\n        ActivityResultContracts.StartActivityForResult()\n    ) { (resultCode, data) ->\n        if (resultCode == Activity.RESULT_OK) runOnDefaultDispatcher {\n            val profile = ProfileManager.getProfile(\n                data!!.getLongExtra(\n                    ProfileSelectActivity.EXTRA_PROFILE_ID, 0\n                )\n            ) ?: return@runOnDefaultDispatcher\n            DataStore.routeOutboundRule = profile.id\n            onMainDispatcher {\n                outbound.value = \"3\"\n            }\n        }\n    }\n\n    val selectAppList = registerForActivityResult(\n        ActivityResultContracts.StartActivityForResult()\n    ) { (_, _) ->\n        apps.postUpdate()\n    }\n\n    lateinit var outbound: OutboundPreference\n    lateinit var reverse: SwitchPreference\n    lateinit var redirect: EditTextPreference\n    lateinit var apps: AppListPreference\n\n    fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n        outbound = findPreference(Key.ROUTE_OUTBOUND)!!\n        reverse = findPreference(Key.ROUTE_REVERSE)!!\n        redirect = findPreference(Key.ROUTE_REDIRECT)!!\n        apps = findPreference(Key.ROUTE_PACKAGES)!!\n\n        fun updateReverse(enabled: Boolean = outbound.value == \"3\") {\n            reverse.isVisible = enabled\n            redirect.isVisible = enabled\n            redirect.isEnabled = reverse.isChecked\n        }\n\n        updateReverse()\n\n        reverse.setOnPreferenceChangeListener { _, newValue ->\n            redirect.isEnabled = newValue as Boolean\n            true\n        }\n\n        outbound.setOnPreferenceChangeListener { _, newValue ->\n            if (newValue.toString() == \"3\") {\n                updateReverse(true)\n                selectProfileForAdd.launch(\n                    Intent(\n                        this@RouteSettingsActivity, ProfileSelectActivity::class.java\n                    )\n                )\n                false\n            } else {\n                updateReverse(false)\n                true\n            }\n        }\n\n        apps.setOnPreferenceClickListener {\n            selectAppList.launch(\n                Intent(\n                    this@RouteSettingsActivity, AppListActivity::class.java\n                )\n            )\n            true\n        }\n    }\n\n    fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {\n        return false\n    }\n\n    class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.unsaved_changes_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                runOnDefaultDispatcher {\n                    (requireActivity() as RouteSettingsActivity).saveAndExit()\n                }\n            }\n            setNegativeButton(R.string.no) { _, _ ->\n                requireActivity().finish()\n            }\n            setNeutralButton(android.R.string.cancel, null)\n        }\n    }\n\n    @Parcelize\n    data class ProfileIdArg(val ruleId: Long) : Parcelable\n    class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.delete_route_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                runOnDefaultDispatcher {\n                    ProfileManager.deleteRule(arg.ruleId)\n                }\n                requireActivity().finish()\n            }\n            setNegativeButton(R.string.no, null)\n        }\n    }\n\n    companion object {\n        const val EXTRA_ROUTE_ID = \"id\"\n        const val EXTRA_PACKAGE_NAME = \"pkg\"\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setTitle(R.string.cag_route)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        if (savedInstanceState == null) {\n            val editingId = intent.getLongExtra(EXTRA_ROUTE_ID, 0L)\n            DataStore.editingId = editingId\n            runOnDefaultDispatcher {\n                if (editingId == 0L) {\n                    init(intent.getStringExtra(EXTRA_PACKAGE_NAME))\n                } else {\n                    val ruleEntity = SagerDatabase.rulesDao.getById(editingId)\n                    if (ruleEntity == null) {\n                        onMainDispatcher {\n                            finish()\n                        }\n                        return@runOnDefaultDispatcher\n                    }\n                    ruleEntity.init()\n                }\n\n                onMainDispatcher {\n                    supportFragmentManager.beginTransaction()\n                        .replace(R.id.settings, MyPreferenceFragmentCompat().apply {\n                            activity = this@RouteSettingsActivity\n                        })\n                        .commit()\n\n                    DataStore.dirty = false\n                    DataStore.profileCacheStore.registerChangeListener(this@RouteSettingsActivity)\n                }\n            }\n\n\n        }\n\n    }\n\n    suspend fun saveAndExit() {\n\n        if (!needSave()) {\n            onMainDispatcher {\n                MaterialAlertDialogBuilder(this@RouteSettingsActivity).setTitle(R.string.empty_route)\n                    .setMessage(R.string.empty_route_notice)\n                    .setPositiveButton(android.R.string.ok, null)\n                    .show()\n            }\n            return\n        }\n\n        val editingId = DataStore.editingId\n        if (editingId == 0L) {\n            if (intent.hasExtra(EXTRA_PACKAGE_NAME)) {\n                setResult(RESULT_OK, Intent())\n            }\n\n            ProfileManager.createRule(RuleEntity().apply { serialize() })\n        } else {\n            val entity = SagerDatabase.rulesDao.getById(DataStore.editingId)\n            if (entity == null) {\n                finish()\n                return\n            }\n            ProfileManager.updateRule(entity.apply { serialize() })\n        }\n        if (editingId == DataStore.selectedProxy && DataStore.directBootAware) DirectBoot.update()\n        finish()\n\n    }\n\n    val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat }\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.profile_config_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)\n\n    override fun onBackPressed() {\n        if (needSave()) {\n            UnsavedChangesDialogFragment().apply { key() }.show(supportFragmentManager, null)\n        } else super.onBackPressed()\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        if (!super.onSupportNavigateUp()) finish()\n        return true\n    }\n\n    override fun onDestroy() {\n        DataStore.profileCacheStore.unregisterChangeListener(this)\n        super.onDestroy()\n    }\n\n    override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {\n        if (key != Key.PROFILE_DIRTY) {\n            DataStore.dirty = true\n        }\n    }\n\n    class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {\n\n        lateinit var activity: RouteSettingsActivity\n\n        override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {\n            preferenceManager.preferenceDataStore = DataStore.profileCacheStore\n            activity.apply {\n                createPreferences(savedInstanceState, rootKey)\n            }\n        }\n\n        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n            super.onViewCreated(view, savedInstanceState)\n\n            ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)\n\n            activity.apply {\n                viewCreated(view, savedInstanceState)\n            }\n        }\n\n        override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {\n            R.id.action_delete -> {\n                if (DataStore.editingId == 0L) {\n                    requireActivity().finish()\n                } else {\n                    DeleteConfirmationDialogFragment().apply {\n                        arg(ProfileIdArg(DataStore.editingId))\n                        key()\n                    }.show(parentFragmentManager, null)\n                }\n                true\n            }\n            R.id.action_apply -> {\n                runOnDefaultDispatcher {\n                    activity.saveAndExit()\n                }\n                true\n            }\n            else -> false\n        }\n\n        override fun onDisplayPreferenceDialog(preference: Preference) {\n            activity.apply {\n                if (displayPreferenceDialog(preference)) return\n            }\n            super.onDisplayPreferenceDialog(preference)\n        }\n\n    }\n\n    object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {\n\n        override fun provideSummary(preference: EditTextPreference): CharSequence {\n            return if (preference.text.isNullOrBlank()) {\n                preference.context.getString(androidx.preference.R.string.not_set)\n            } else {\n                \"\\u2022\".repeat(preference.text.length)\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.pm.ActivityInfo\nimport android.content.pm.ShortcutManager\nimport android.graphics.ImageDecoder\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.provider.MediaStore\nimport android.view.KeyEvent\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.content.getSystemService\nimport androidx.core.view.isGone\nimport com.google.android.material.snackbar.Snackbar\nimport com.google.zxing.BinaryBitmap\nimport com.google.zxing.DecodeHintType\nimport com.google.zxing.NotFoundException\nimport com.google.zxing.RGBLuminanceSource\nimport com.google.zxing.common.GlobalHistogramBinarizer\nimport com.google.zxing.qrcode.QRCodeReader\nimport com.journeyapps.barcodescanner.BarcodeCallback\nimport com.journeyapps.barcodescanner.BarcodeResult\nimport com.journeyapps.barcodescanner.CaptureManager\nimport com.journeyapps.barcodescanner.MixedDecoder\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.databinding.LayoutScannerBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.widget.ListHolderListener\n\n\nclass ScannerActivity : ThemedActivity(),\n    BarcodeCallback {\n\n    lateinit var capture: CaptureManager\n    lateinit var binding: LayoutScannerBinding\n\n    @SuppressLint(\"SourceLockedOrientationActivity\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) {\n            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT\n        }\n        if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed(\"scan\")\n        binding = LayoutScannerBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        ListHolderListener.setup(this)\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        binding.barcodeScanner.statusView.isGone = true\n        binding.barcodeScanner.viewFinder.isGone = true\n        binding.barcodeScanner.barcodeView.setDecoderFactory {\n            MixedDecoder(QRCodeReader())\n        }\n\n        capture = CaptureManager(this, binding.barcodeScanner)\n        binding.barcodeScanner.decodeSingle(this)\n    }\n\n    override fun snackbarInternal(text: CharSequence): Snackbar {\n        return Snackbar.make(binding.barcodeScanner, text, Snackbar.LENGTH_LONG)\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.scanner_menu, menu)\n        return true\n    }\n\n    val importCodeFile = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) {\n        runOnDefaultDispatcher {\n            try {\n                it.forEachTry { uri ->\n                    val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                        ImageDecoder.decodeBitmap(\n                            ImageDecoder.createSource(\n                                contentResolver, uri\n                            )\n                        ) { decoder, _, _ ->\n                            decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE\n                            decoder.isMutableRequired = true\n                        }\n                    } else {\n                        @Suppress(\"DEPRECATION\") MediaStore.Images.Media.getBitmap(\n                            contentResolver, uri\n                        )\n                    }\n                    val intArray = IntArray(bitmap.width * bitmap.height)\n                    bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)\n\n                    val source = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)\n                    val qrReader = QRCodeReader()\n                    try {\n                        val result = try {\n                            qrReader.decode(\n                                BinaryBitmap(GlobalHistogramBinarizer(source)),\n                                mapOf(DecodeHintType.TRY_HARDER to true)\n                            )\n                        } catch (e: NotFoundException) {\n                            qrReader.decode(\n                                BinaryBitmap(GlobalHistogramBinarizer(source.invert())),\n                                mapOf(DecodeHintType.TRY_HARDER to true)\n                            )\n                        }\n\n                        val results = parseProxies(result.text ?: \"\")\n\n                        if (results.isNotEmpty()) {\n                            onMainDispatcher {\n                                finish()\n                            }\n                            val currentGroupId = DataStore.selectedGroupForImport()\n                            if (DataStore.selectedGroup != currentGroupId) {\n                                DataStore.selectedGroup = currentGroupId\n                            }\n\n                            for (profile in results) {\n                                ProfileManager.createProfile(currentGroupId, profile)\n                            }\n                        } else {\n                            Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT)\n                                    .show()\n                        }\n                    } catch (e: SubscriptionFoundException) {\n                        startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply {\n                            action = Intent.ACTION_VIEW\n                            data = Uri.parse(e.link)\n                        })\n                        finish()\n                    } catch (e: Throwable) {\n                        Logs.w(e)\n                        onMainDispatcher {\n                            Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT)\n                                    .show()\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                Logs.w(e)\n\n                onMainDispatcher {\n                    Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()\n                }\n            }\n        }\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        return if (item.itemId == R.id.action_import_file) {\n            startFilesForResult(importCodeFile, \"image/*\")\n            true\n        } else {\n            super.onOptionsItemSelected(item)\n        }\n    }\n\n    /**\n     * See also: https://stackoverflow.com/a/31350642/2245107\n     */\n    override fun shouldUpRecreateTask(targetIntent: Intent?) =\n        super.shouldUpRecreateTask(targetIntent) || isTaskRoot\n\n    override fun onResume() {\n        super.onResume()\n        capture.onResume()\n    }\n\n    override fun onPause() {\n        super.onPause()\n        capture.onPause()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        capture.onDestroy()\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        capture.onSaveInstanceState(outState)\n    }\n\n    override fun onRequestPermissionsResult(\n        requestCode: Int,\n        permissions: Array<String>,\n        grantResults: IntArray,\n    ) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        capture.onRequestPermissionsResult(requestCode, permissions, grantResults)\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {\n        return binding.barcodeScanner.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)\n    }\n\n    override fun barcodeResult(result: BarcodeResult) {\n        finish()\n        val text = result.result.text\n        runOnDefaultDispatcher {\n            try {\n                val results = parseProxies(text)\n                if (results.isNotEmpty()) {\n                    val currentGroupId = DataStore.selectedGroupForImport()\n                    if (DataStore.selectedGroup != currentGroupId) {\n                        DataStore.selectedGroup = currentGroupId\n                    }\n\n                    for (profile in results) {\n                        ProfileManager.createProfile(currentGroupId, profile)\n                    }\n                } else {\n                    Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT).show()\n                }\n            } catch (e: SubscriptionFoundException) {\n                startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply {\n                    action = Intent.ACTION_VIEW\n                    data = Uri.parse(e.link)\n                })\n                finish()\n            } catch (e: Throwable) {\n                Logs.w(e)\n                onMainDispatcher {\n                    Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT).show()\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/SettingsFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.core.view.ViewCompat\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.widget.ListHolderListener\n\nclass SettingsFragment : ToolbarFragment(R.layout.layout_config_settings) {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)\n        toolbar.setTitle(R.string.settings)\n\n        parentFragmentManager.beginTransaction()\n            .replace(R.id.settings, SettingsPreferenceFragment())\n            .commitAllowingStateLoss()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.View\nimport androidx.core.app.ActivityCompat\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport androidx.preference.SwitchPreference\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.TunImplementation\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.utils.Theme\nimport io.nekohasekai.sagernet.widget.ColorPickerPreference\nimport java.io.File\n\nclass SettingsPreferenceFragment : PreferenceFragmentCompat() {\n\n    private lateinit var isProxyApps: SwitchPreference\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        listView.layoutManager = FixedLinearLayoutManager(listView)\n    }\n\n    val reloadListener = Preference.OnPreferenceChangeListener { _, _ ->\n        needReload()\n        true\n    }\n\n    override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {\n        preferenceManager.preferenceDataStore = DataStore.configurationStore\n        DataStore.initGlobal()\n        addPreferencesFromResource(R.xml.global_preferences)\n        val appTheme = findPreference<ColorPickerPreference>(Key.APP_THEME)!!\n        if (!isExpert) {\n            appTheme.remove()\n        } else {\n            appTheme.setOnPreferenceChangeListener { _, newTheme ->\n                if (SagerNet.started) {\n                    SagerNet.reloadService()\n                }\n                val theme = Theme.getTheme(newTheme as Int)\n                app.setTheme(theme)\n                requireActivity().apply {\n                    setTheme(theme)\n                    ActivityCompat.recreate(this)\n                }\n                true\n            }\n        }\n        val nightTheme = findPreference<SimpleMenuPreference>(Key.NIGHT_THEME)!!\n        nightTheme.setOnPreferenceChangeListener { _, newTheme ->\n            Theme.currentNightMode = (newTheme as String).toInt()\n            Theme.applyNightTheme()\n            true\n        }\n        val portSocks5 = findPreference<EditTextPreference>(Key.SOCKS_PORT)!!\n        val speedInterval = findPreference<Preference>(Key.SPEED_INTERVAL)!!\n        val serviceMode = findPreference<Preference>(Key.SERVICE_MODE)!!\n        val allowAccess = findPreference<Preference>(Key.ALLOW_ACCESS)!!\n        val requireHttp = findPreference<SwitchPreference>(Key.REQUIRE_HTTP)!!\n        val appendHttpProxy = findPreference<SwitchPreference>(Key.APPEND_HTTP_PROXY)!!\n        val portHttp = findPreference<EditTextPreference>(Key.HTTP_PORT)!!\n        when {\n            Build.VERSION.SDK_INT < Build.VERSION_CODES.N -> {\n                requireHttp.remove()\n                appendHttpProxy.remove()\n                portHttp.setIcon(R.drawable.ic_baseline_http_24)\n                portHttp.onPreferenceChangeListener = reloadListener\n            }\n            Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> {\n                portHttp.isEnabled = requireHttp.isChecked\n                appendHttpProxy.remove()\n                requireHttp.setOnPreferenceChangeListener { _, newValue ->\n                    portHttp.isEnabled = newValue as Boolean\n                    needReload()\n                    true\n                }\n            }\n            else -> {\n                portHttp.isEnabled = requireHttp.isChecked\n                appendHttpProxy.isEnabled = requireHttp.isChecked\n                requireHttp.setOnPreferenceChangeListener { _, newValue ->\n                    portHttp.isEnabled = newValue as Boolean\n                    appendHttpProxy.isEnabled = newValue as Boolean\n                    needReload()\n                    true\n                }\n            }\n        }\n\n        val portLocalDns = findPreference<EditTextPreference>(Key.LOCAL_DNS_PORT)!!\n\n        val showStopButton = findPreference<SwitchPreference>(Key.SHOW_STOP_BUTTON)!!\n        if (Build.VERSION.SDK_INT < 24) {\n            showStopButton.remove()\n        }\n        val showDirectSpeed = findPreference<SwitchPreference>(Key.SHOW_DIRECT_SPEED)!!\n        val ipv6Mode = findPreference<Preference>(Key.IPV6_MODE)!!\n        val domainStrategy = findPreference<Preference>(Key.DOMAIN_STRATEGY)!!\n        val trafficSniffing = findPreference<Preference>(Key.TRAFFIC_SNIFFING)!!\n        val enableMux = findPreference<Preference>(Key.ENABLE_MUX)!!\n        val enableMuxForAll = findPreference<Preference>(Key.ENABLE_MUX_FOR_ALL)!!\n        val muxConcurrency = findPreference<EditTextPreference>(Key.MUX_CONCURRENCY)!!\n        val tcpKeepAliveInterval = findPreference<EditTextPreference>(Key.TCP_KEEP_ALIVE_INTERVAL)!!\n\n        val bypassLan = findPreference<SwitchPreference>(Key.BYPASS_LAN)!!\n        val bypassLanInCoreOnly = findPreference<SwitchPreference>(Key.BYPASS_LAN_IN_CORE_ONLY)!!\n\n        bypassLanInCoreOnly.isEnabled = bypassLan.isChecked\n        bypassLan.setOnPreferenceChangeListener { _, newValue ->\n            bypassLanInCoreOnly.isEnabled = newValue as Boolean\n            needReload()\n            true\n        }\n\n        val remoteDns = findPreference<EditTextPreference>(Key.REMOTE_DNS)!!\n        val directDns = findPreference<EditTextPreference>(Key.DIRECT_DNS)!!\n        val enableDnsRouting = findPreference<SwitchPreference>(Key.ENABLE_DNS_ROUTING)!!\n        val enableFakeDns = findPreference<SwitchPreference>(Key.ENABLE_FAKEDNS)!!\n\n        val requireTransproxy = findPreference<SwitchPreference>(Key.REQUIRE_TRANSPROXY)!!\n        val transproxyPort = findPreference<EditTextPreference>(Key.TRANSPROXY_PORT)!!\n        val transproxyMode = findPreference<SimpleMenuPreference>(Key.TRANSPROXY_MODE)!!\n        val enableLog = findPreference<SwitchPreference>(Key.ENABLE_LOG)!!\n\n        transproxyPort.isEnabled = requireTransproxy.isChecked\n        transproxyMode.isEnabled = requireTransproxy.isChecked\n\n        requireTransproxy.setOnPreferenceChangeListener { _, newValue ->\n            transproxyPort.isEnabled = newValue as Boolean\n            transproxyMode.isEnabled = newValue\n            needReload()\n            true\n        }\n\n        val providerTrojan = findPreference<SimpleMenuPreference>(Key.PROVIDER_TROJAN)!!\n        val providerShadowsocksAEAD = findPreference<SimpleMenuPreference>(Key.PROVIDER_SS_AEAD)!!\n        val providerShadowsocksStream = findPreference<SimpleMenuPreference>(Key.PROVIDER_SS_STREAM)!!\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n            providerShadowsocksAEAD.setEntries(R.array.ss_aead_provider_api21)\n            providerShadowsocksAEAD.setEntryValues(R.array.ss_aead_provider_api21_values)\n            providerShadowsocksStream.setEntries(R.array.ss_stream_provider_api21)\n            providerShadowsocksStream.setEntryValues(R.array.ss_stream_provider_api21_values)\n        }\n\n        if (!isExpert) {\n            providerTrojan.setEntries(R.array.trojan_provider)\n            providerTrojan.setEntryValues(R.array.trojan_provider_value)\n        }\n\n        val dnsHosts = findPreference<EditTextPreference>(Key.DNS_HOSTS)!!\n\n        portLocalDns.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        muxConcurrency.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        portSocks5.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        portHttp.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        dnsHosts.setOnBindEditTextListener(EditTextPreferenceModifiers.Hosts)\n\n        val metedNetwork = findPreference<Preference>(Key.METERED_NETWORK)!!\n        if (Build.VERSION.SDK_INT < 28) {\n            metedNetwork.remove()\n        }\n        isProxyApps = findPreference(Key.PROXY_APPS)!!\n        isProxyApps.setOnPreferenceChangeListener { _, newValue ->\n            startActivity(Intent(activity, AppManagerActivity::class.java))\n            if (newValue as Boolean) DataStore.dirty = true\n            newValue\n        }\n\n        val utlsFingerprint = findPreference<SimpleMenuPreference>(Key.UTLS_FINGERPRINT)!!\n        val appTrafficStatistics = findPreference<SwitchPreference>(Key.APP_TRAFFIC_STATISTICS)!!\n        val profileTrafficStatistics = findPreference<SwitchPreference>(Key.PROFILE_TRAFFIC_STATISTICS)!!\n        speedInterval.isEnabled = profileTrafficStatistics.isChecked\n        profileTrafficStatistics.setOnPreferenceChangeListener { _, newValue ->\n            speedInterval.isEnabled = newValue as Boolean\n            true\n        }\n\n        serviceMode.setOnPreferenceChangeListener { _, _ ->\n            if (SagerNet.started) SagerNet.stopService()\n            true\n        }\n\n        val tunImplementation = findPreference<SimpleMenuPreference>(Key.TUN_IMPLEMENTATION)!!\n        val destinationOverride = findPreference<SwitchPreference>(Key.DESTINATION_OVERRIDE)!!\n        val resolveDestination = findPreference<SwitchPreference>(Key.RESOLVE_DESTINATION)!!\n        val enablePcap = findPreference<SwitchPreference>(Key.ENABLE_PCAP)!!\n\n        speedInterval.onPreferenceChangeListener = reloadListener\n        portSocks5.onPreferenceChangeListener = reloadListener\n        portHttp.onPreferenceChangeListener = reloadListener\n        appendHttpProxy.onPreferenceChangeListener = reloadListener\n        showStopButton.onPreferenceChangeListener = reloadListener\n        showDirectSpeed.onPreferenceChangeListener = reloadListener\n        domainStrategy.onPreferenceChangeListener = reloadListener\n        trafficSniffing.onPreferenceChangeListener = reloadListener\n        enableMux.onPreferenceChangeListener = reloadListener\n        enableMuxForAll.onPreferenceChangeListener = reloadListener\n        muxConcurrency.onPreferenceChangeListener = reloadListener\n        tcpKeepAliveInterval.onPreferenceChangeListener = reloadListener\n        bypassLanInCoreOnly.onPreferenceChangeListener = reloadListener\n\n        remoteDns.onPreferenceChangeListener = reloadListener\n        directDns.onPreferenceChangeListener = reloadListener\n        enableDnsRouting.onPreferenceChangeListener = reloadListener\n        enableFakeDns.onPreferenceChangeListener = reloadListener\n        dnsHosts.onPreferenceChangeListener = reloadListener\n\n        portLocalDns.onPreferenceChangeListener = reloadListener\n        ipv6Mode.onPreferenceChangeListener = reloadListener\n        allowAccess.onPreferenceChangeListener = reloadListener\n\n        transproxyPort.onPreferenceChangeListener = reloadListener\n        transproxyMode.onPreferenceChangeListener = reloadListener\n\n        enableLog.onPreferenceChangeListener = reloadListener\n        providerTrojan.onPreferenceChangeListener = reloadListener\n        providerShadowsocksAEAD.onPreferenceChangeListener = reloadListener\n        providerShadowsocksStream.onPreferenceChangeListener = reloadListener\n        utlsFingerprint.onPreferenceChangeListener = reloadListener\n        appTrafficStatistics.onPreferenceChangeListener = reloadListener\n        tunImplementation.onPreferenceChangeListener = reloadListener\n        destinationOverride.onPreferenceChangeListener = reloadListener\n        resolveDestination.onPreferenceChangeListener = reloadListener\n        enablePcap.setOnPreferenceChangeListener { _, newValue ->\n            if (newValue as Boolean) {\n                val path = File(app.externalAssets, \"pcap\").absolutePath\n                MaterialAlertDialogBuilder(requireContext()).apply {\n                    setTitle(R.string.pcap)\n                    setMessage(resources.getString(R.string.pcap_notice, path))\n                    setPositiveButton(android.R.string.ok) { _, _ ->\n                        needReload()\n                    }\n                    setNegativeButton(android.R.string.copy) { _, _ ->\n                        SagerNet.trySetPrimaryClip(path)\n                        snackbar(R.string.copy_success).show()\n                    }\n                }.show()\n                if (tunImplementation.value != \"${TunImplementation.GVISOR}\") {\n                    tunImplementation.value = \"${TunImplementation.GVISOR}\"\n                }\n            } else needReload()\n            true\n        }\n\n    }\n\n    override fun onResume() {\n        super.onResume()\n\n        if (::isProxyApps.isInitialized) {\n            isProxyApps.isChecked = DataStore.proxyApps\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/StatsFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.text.format.Formatter\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.core.view.isVisible\nimport androidx.fragment.app.Fragment\nimport androidx.recyclerview.widget.RecyclerView\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.aidl.AppStats\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.databinding.LayoutTrafficItemBinding\nimport io.nekohasekai.sagernet.databinding.LayoutTrafficListBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.utils.PackageCache\n\nclass StatsFragment : Fragment(R.layout.layout_traffic_list) {\n\n    lateinit var binding: LayoutTrafficListBinding\n    lateinit var adapter: ActiveAdapter\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        binding = LayoutTrafficListBinding.bind(view)\n        adapter = ActiveAdapter()\n        binding.trafficList.layoutManager = FixedLinearLayoutManager(binding.trafficList)\n        binding.trafficList.adapter = adapter\n\n        (parentFragment as TrafficFragment).listeners.add(::emitStats)\n\n        runOnDefaultDispatcher {\n            emitStats(emptyList())\n        }\n    }\n\n    fun emitStats(statsList: List<AppStats>) {\n        var data = statsList.associate { it.packageName to it.copy() }.toMutableMap()\n        for (stats in SagerDatabase.statsDao.all()) {\n            if (data.containsKey(stats.packageName)) {\n                data[stats.packageName]!! += stats\n            } else {\n                data[stats.packageName] = stats.toStats()\n            }\n        }\n        for (stats in data.values) {\n            stats.tcpConnectionsTotal += stats.tcpConnections\n            stats.udpConnectionsTotal += stats.udpConnections\n            stats.uplinkTotal += stats.uplink\n            stats.downlinkTotal += stats.downlink\n        }\n        if (data.isEmpty()) {\n            runOnMainDispatcher {\n                binding.holder.isVisible = true\n                binding.trafficList.isVisible = false\n\n                if (!SagerNet.started || DataStore.serviceMode != Key.MODE_VPN) {\n                    binding.holder.text = getString(R.string.traffic_holder)\n                } else if ((activity as MainActivity).connection.service?.trafficStatsEnabled != true) {\n                    binding.holder.text = getString(R.string.app_statistics_disabled)\n                } else {\n                    binding.holder.text = getString(R.string.no_statistics)\n                }\n            }\n            binding.trafficList.post {\n                adapter.data = emptyList()\n                adapter.notifyDataSetChanged()\n            }\n        } else {\n            runOnMainDispatcher {\n                binding.holder.isVisible = false\n                binding.trafficList.isVisible = true\n            }\n            data = data.toSortedMap { ka, kb ->\n                val a = data[ka]!!\n                val b = data[kb]!!\n                val dataA = a.uplinkTotal + a.downlinkTotal\n                val dataB = b.uplinkTotal + b.downlinkTotal\n                if (dataA != dataB) {\n                    dataB.compareTo(dataA)\n                } else {\n                    val connA = a.tcpConnectionsTotal + a.udpConnectionsTotal\n                    val connB = b.tcpConnectionsTotal + b.udpConnectionsTotal\n                    if (connA != connB) {\n                        connB.compareTo(connA)\n                    } else {\n                        b.packageName.compareTo(a.packageName)\n                    }\n                }\n            }\n            binding.trafficList.post {\n                adapter.data = data.values.toList()\n                adapter.notifyDataSetChanged()\n            }\n        }\n    }\n\n    inner class ActiveAdapter : RecyclerView.Adapter<ActiveViewHolder>() {\n\n        init {\n            setHasStableIds(true)\n        }\n\n        lateinit var data: List<AppStats>\n\n        override fun getItemId(position: Int): Long {\n            return data[position].uid.toLong()\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActiveViewHolder {\n            return ActiveViewHolder(\n                LayoutTrafficItemBinding.inflate(layoutInflater, parent, false)\n            )\n        }\n\n        override fun onBindViewHolder(holder: ActiveViewHolder, position: Int) {\n            holder.bind(data[position])\n        }\n\n        override fun getItemCount(): Int {\n            if (!::data.isInitialized) return 0\n            return data.size\n        }\n    }\n\n    inner class ActiveViewHolder(val binding: LayoutTrafficItemBinding) : RecyclerView.ViewHolder(\n        binding.root\n    ) {\n\n        lateinit var stats: AppStats\n\n        fun bind(stats: AppStats) {\n            this.stats = stats\n            PackageCache.awaitLoadSync()\n\n            val packageName = if (stats.uid > 1000) {\n                PackageCache.uidMap[stats.uid]?.iterator()?.next() ?: \"android\"\n            } else {\n                \"android\"\n            }\n\n            binding.menu.setOnClickListener {\n                val popup = PopupMenu(requireContext(), it)\n                popup.menuInflater.inflate(R.menu.traffic_item_menu, popup.menu)\n                popup.setOnMenuItemClickListener(\n                    (requireParentFragment() as TrafficFragment).ItemMenuListener(\n                        stats\n                    )\n                )\n                popup.show()\n            }\n\n            binding.label.text = PackageCache.loadLabel(packageName)\n            binding.desc.text = \"$packageName (${stats.uid})\"\n            binding.tcpConnections.text = getString(\n                R.string.tcp_connections, stats.tcpConnectionsTotal\n            )\n            binding.udpConnections.text = getString(\n                R.string.udp_connections, stats.udpConnectionsTotal\n            )\n            binding.trafficUplink.text = getString(\n                R.string.traffic_uplink_total,\n                Formatter.formatFileSize(requireContext(), stats.uplinkTotal),\n            )\n            binding.trafficDownlink.text = getString(\n                R.string.traffic_downlink_total,\n                Formatter.formatFileSize(requireContext(), stats.downlinkTotal),\n            )\n            val info = PackageCache.installedApps[packageName]\n            if (info != null) runOnDefaultDispatcher {\n                try {\n                    val icon = info.loadIcon(app.packageManager)\n                    onMainDispatcher {\n                        binding.icon.setImageDrawable(icon)\n                    }\n                } catch (ignored: Exception) {\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.content.res.Configuration\nimport android.os.Bundle\nimport android.widget.TextView\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.app.ActivityCompat\nimport com.google.android.material.snackbar.Snackbar\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.utils.Theme\n\nabstract class ThemedActivity : AppCompatActivity {\n    constructor() : super()\n    constructor(contentLayoutId: Int) : super(contentLayoutId)\n\n    var themeResId = 0\n    var uiMode = 0\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        Theme.apply(this)\n        Theme.applyNightTheme()\n\n        super.onCreate(savedInstanceState)\n\n        uiMode = resources.configuration.uiMode\n    }\n\n    override fun setTheme(resId: Int) {\n        super.setTheme(resId)\n\n        themeResId = resId\n    }\n\n    override fun onConfigurationChanged(newConfig: Configuration) {\n        super.onConfigurationChanged(newConfig)\n\n        if (newConfig.uiMode != uiMode) {\n            uiMode = newConfig.uiMode\n\n            if (DataStore.appTheme == Theme.BLACK) {\n                Theme.apply(this)\n            }\n\n            ActivityCompat.recreate(this)\n        }\n    }\n\n    fun snackbar(@StringRes resId: Int): Snackbar = snackbar(\"\").setText(resId)\n    fun snackbar(text: CharSequence): Snackbar = snackbarInternal(text).apply {\n        view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text).apply {\n            maxLines = 10\n        }\n    }\n    internal open fun snackbarInternal(text: CharSequence): Snackbar = throw NotImplementedError()\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ToolbarFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.view.KeyEvent\nimport android.view.View\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.view.GravityCompat\nimport androidx.fragment.app.Fragment\nimport io.nekohasekai.sagernet.R\n\nopen class ToolbarFragment : Fragment {\n\n    constructor() : super()\n    constructor(contentLayoutId: Int) : super(contentLayoutId)\n\n    lateinit var toolbar: Toolbar\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        toolbar = view.findViewById(R.id.toolbar)\n        toolbar.setNavigationIcon(R.drawable.ic_navigation_menu)\n        toolbar.setNavigationOnClickListener {\n            (activity as MainActivity).binding.drawerLayout.openDrawer(GravityCompat.START)\n        }\n    }\n\n    open fun onKeyDown(ketCode: Int, event: KeyEvent) = false\n    open fun onBackPressed(): Boolean = false\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport com.google.android.material.tabs.TabLayoutMediator\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.databinding.LayoutToolsBinding\nimport io.nekohasekai.sagernet.ktx.isExpert\n\nclass ToolsFragment : ToolbarFragment(R.layout.layout_tools) {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        toolbar.setTitle(R.string.menu_tools)\n\n        val tools = mutableListOf<NamedFragment>()\n        tools.add(CloudflareFragment())\n\n        if (BuildConfig.DEBUG || isExpert) tools.add(DebugFragment())\n\n        val binding = LayoutToolsBinding.bind(view)\n        binding.toolsPager.adapter = ToolsAdapter(tools)\n\n        TabLayoutMediator(binding.toolsTab, binding.toolsPager) { tab, position ->\n            tab.text = tools[position].name()\n            tab.view.setOnLongClickListener { // clear toast\n                true\n            }\n        }.attach()\n    }\n\n    inner class ToolsAdapter(val tools: List<Fragment>) : FragmentStateAdapter(this) {\n\n        override fun getItemCount() = tools.size\n\n        override fun createFragment(position: Int) = tools[position]\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/TrafficFragment.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.app.Activity\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.provider.Settings\nimport android.view.MenuItem\nimport android.view.View\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.appcompat.widget.Toolbar\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentStateAdapter\nimport com.google.android.material.tabs.TabLayoutMediator\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.aidl.AppStats\nimport io.nekohasekai.sagernet.databinding.LayoutTrafficBinding\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.utils.PackageCache\n\nclass TrafficFragment : ToolbarFragment(R.layout.layout_traffic),\n    Toolbar.OnMenuItemClickListener {\n\n    lateinit var binding: LayoutTrafficBinding\n    lateinit var adapter: TrafficAdapter\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        toolbar.setTitle(R.string.menu_traffic)\n        toolbar.inflateMenu(R.menu.traffic_menu)\n        toolbar.setOnMenuItemClickListener(this)\n\n        binding = LayoutTrafficBinding.bind(view)\n        adapter = TrafficAdapter()\n        binding.trafficPager.adapter = adapter\n\n        TabLayoutMediator(binding.trafficTab, binding.trafficPager) { tab, position ->\n            tab.text = when (position) {\n                0 -> getString(R.string.traffic_active)\n                else -> getString(R.string.traffic_stats)\n            }\n            tab.view.setOnLongClickListener { // clear toast\n                true\n            }\n        }.attach()\n\n        (requireActivity() as MainActivity).connection.trafficTimeout = 1500\n    }\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_clear_traffic_statistics -> {\n                (requireActivity() as MainActivity).connection.service?.resetTrafficStats()\n                runOnDefaultDispatcher {\n                    emitStats(emptyList())\n                }\n            }\n        }\n        return true\n    }\n\n    inner class TrafficAdapter() : FragmentStateAdapter(this) {\n\n        override fun getItemCount(): Int {\n            return 2\n        }\n\n        override fun createFragment(position: Int): Fragment {\n            return when (position) {\n                0 -> ActiveFragment()\n                else -> StatsFragment()\n            }\n        }\n\n    }\n\n    val listeners = mutableListOf<(List<AppStats>) -> Unit>()\n\n    fun emitStats(statsList: List<AppStats>) {\n        runOnDefaultDispatcher {\n            for (listener in listeners) listener(statsList)\n        }\n    }\n\n    override fun onStart() {\n        super.onStart()\n\n        (requireActivity() as MainActivity).connection.trafficTimeout = 1500\n    }\n\n    override fun onStop() {\n        super.onStop()\n\n        (requireActivity() as MainActivity).connection.trafficTimeout = 0\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n\n        (requireActivity() as MainActivity).connection.trafficTimeout = 0\n    }\n\n    val createRule = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {\n        if (it.resultCode == Activity.RESULT_OK) {\n            (requireActivity() as MainActivity).ruleCreated()\n        }\n    }\n\n    inner class ItemMenuListener(val stats: AppStats) : PopupMenu.OnMenuItemClickListener {\n        override fun onMenuItemClick(item: MenuItem): Boolean {\n            when (item.itemId) {\n                R.id.copy_label -> {\n                    val success = SagerNet.trySetPrimaryClip(PackageCache.loadLabel(stats.packageName))\n                    snackbar(if (success) R.string.copy_success else R.string.copy_failed).show()\n                }\n                R.id.copy_package_name -> {\n                    val success = SagerNet.trySetPrimaryClip(stats.packageName)\n                    snackbar(if (success) R.string.copy_success else R.string.copy_failed).show()\n                }\n\n                R.id.open_app -> {\n                    try {\n                        val launch = app.packageManager.getLaunchIntentForPackage(stats.packageName)\n                        if (launch == null) {\n                            snackbar(R.string.app_no_launcher).show()\n                        } else {\n                            startActivity(launch)\n                        }\n                    } catch (e: Exception) {\n                        snackbar(e.readableMessage).show()\n                    }\n                }\n                R.id.open_settings -> {\n                    try {\n                        startActivity(Intent().apply {\n                            action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS\n                            data = Uri.fromParts(\n                                \"package\", stats.packageName, null\n                            )\n                        })\n                    } catch (e: Exception) {\n                        snackbar(e.readableMessage).show()\n                    }\n                }\n                R.id.open_market -> {\n                    try {\n                        startActivity(\n                            Intent(\n                                Intent.ACTION_VIEW,\n                                Uri.parse(\"market://details?id=${stats.packageName}\")\n                            )\n                        )\n                    } catch (e: ActivityNotFoundException) {\n                        requireContext().launchCustomTab(\"https://play.google.com/store/apps/details?id=${stats.packageName}\")\n                    }\n                }\n                R.id.create_rule -> {\n                    createRule.launch(Intent(\n                        requireContext(), RouteSettingsActivity::class.java\n                    ).apply {\n                        putExtra(RouteSettingsActivity.EXTRA_PACKAGE_NAME, stats.packageName)\n                    })\n                }\n            }\n            return true\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/VpnRequestActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui\n\nimport android.app.Activity\nimport android.app.KeyguardManager\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.net.VpnService\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContract\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.getSystemService\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.broadcastReceiver\n\nclass VpnRequestActivity : AppCompatActivity() {\n    private var receiver: BroadcastReceiver? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {\n            receiver = broadcastReceiver { _, _ -> connect.launch(null) }\n            registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))\n        } else connect.launch(null)\n    }\n\n    private val connect = registerForActivityResult(StartService()) {\n        if (it) Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()\n        finish()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        if (receiver != null) unregisterReceiver(receiver)\n    }\n\n    class StartService : ActivityResultContract<Void?, Boolean>() {\n        private var cachedIntent: Intent? = null\n\n        override fun getSynchronousResult(\n            context: Context,\n            input: Void?,\n        ): SynchronousResult<Boolean>? {\n            if (DataStore.serviceMode == Key.MODE_VPN) VpnService.prepare(context)?.let { intent ->\n                cachedIntent = intent\n                return null\n            }\n            SagerNet.startService()\n            return SynchronousResult(false)\n        }\n\n        override fun createIntent(context: Context, input: Void?) =\n            cachedIntent!!.also { cachedIntent = null }\n\n        override fun parseResult(resultCode: Int, intent: Intent?) =\n            if (resultCode == Activity.RESULT_OK) {\n                SagerNet.startService()\n                false\n            } else {\n                Logs.e(\"Failed to start VpnService: $intent\")\n                true\n            }\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/BalancerSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Intent\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.text.format.Formatter\nimport android.text.method.LinkMovementMethod\nimport android.text.util.Linkify\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.activity.result.component1\nimport androidx.activity.result.component2\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.view.isVisible\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.databinding.LayoutAddEntityBinding\nimport io.nekohasekai.sagernet.databinding.LayoutProfileBinding\nimport io.nekohasekai.sagernet.fmt.internal.BalancerBean\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.ui.ProfileSelectActivity\nimport io.nekohasekai.sagernet.widget.GroupPreference\n\n@SuppressLint(\"Registered\")\nclass BalancerSettingsActivity :\n    ProfileSettingsActivity<BalancerBean>(R.layout.layout_chain_settings) {\n\n    override fun createEntity() = BalancerBean()\n\n    val proxyList = ArrayList<ProxyEntity>()\n\n    override fun BalancerBean.init() {\n        DataStore.profileName = name\n        DataStore.balancerType = type\n        DataStore.balancerStrategy = strategy\n        DataStore.balancerGroup = groupId\n        DataStore.serverProtocol = proxies.joinToString(\",\")\n    }\n\n    override fun BalancerBean.serialize() {\n        name = DataStore.profileName\n        type = DataStore.balancerType\n        strategy = DataStore.balancerStrategy\n        groupId = DataStore.balancerGroup\n        proxies = proxyList.map { it.id }\n        initializeDefaultValues()\n    }\n\n    lateinit var balancerType: SimpleMenuPreference\n    lateinit var balancerGroup: GroupPreference\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.balancer_preferences)\n\n        balancerType = findPreference(Key.BALANCER_TYPE)!!\n        balancerGroup = findPreference(Key.BALANCER_GROUP)!!\n        itemView = findViewById(R.id.list_cell)\n\n        balancerType.setOnPreferenceChangeListener { _, newValue ->\n            updateType(newValue.toString().toInt())\n            true\n        }\n    }\n\n    fun updateType(type: Int = DataStore.balancerType) {\n        when (type) {\n            BalancerBean.TYPE_LIST -> {\n                balancerGroup.isVisible = false\n                configurationList.isVisible = true\n                itemView.isVisible = true\n            }\n            BalancerBean.TYPE_GROUP -> {\n                balancerGroup.isVisible = true\n                configurationList.isVisible = false\n                itemView.isVisible = false\n            }\n        };\n    }\n\n    lateinit var itemView: LinearLayout\n    lateinit var configurationList: RecyclerView\n    lateinit var configurationAdapter: ProxiesAdapter\n    lateinit var layoutManager: LinearLayoutManager\n\n    @SuppressLint(\"InlinedApi\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        supportActionBar!!.setTitle(R.string.balancer_settings)\n        configurationList = findViewById(R.id.configuration_list)\n        layoutManager = FixedLinearLayoutManager(configurationList)\n        configurationList.layoutManager = layoutManager\n        configurationAdapter = ProxiesAdapter()\n        configurationList.adapter = configurationAdapter\n\n        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(\n            ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START\n        ) {\n            override fun getSwipeDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) = if (viewHolder is ProfileHolder) {\n                super.getSwipeDirs(recyclerView, viewHolder)\n            } else 0\n\n            override fun getDragDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) = if (viewHolder is ProfileHolder) {\n                super.getDragDirs(recyclerView, viewHolder)\n            } else 0\n\n            override fun onMove(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n                target: RecyclerView.ViewHolder,\n            ): Boolean {\n                return if (target !is ProfileHolder) false else {\n                    configurationAdapter.move(\n                        viewHolder.bindingAdapterPosition, target.bindingAdapterPosition\n                    )\n                    true\n                }\n            }\n\n            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n                configurationAdapter.remove(viewHolder.bindingAdapterPosition)\n            }\n\n        }).attachToRecyclerView(configurationList)\n\n    }\n\n    override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n        view.rootView.findViewById<RecyclerView>(R.id.recycler_view).apply {\n            (layoutParams ?: LinearLayout.LayoutParams(-1, -2)).apply {\n                height = -2\n                layoutParams = this\n            }\n        }\n\n        runOnDefaultDispatcher {\n            configurationAdapter.reload()\n        }\n\n        updateType()\n    }\n\n    inner class ProxiesAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {\n\n        suspend fun reload() {\n            val idList = DataStore.serverProtocol.split(\",\")\n                .mapNotNull { it.takeIf { it.isNotBlank() }?.toLong() }\n            if (idList.isNotEmpty()) {\n                val profiles = ProfileManager.getProfiles(idList).map { it.id to it }.toMap()\n                for (id in idList) {\n                    proxyList.add(profiles[id] ?: continue)\n                }\n            }\n            onMainDispatcher {\n                notifyDataSetChanged()\n            }\n        }\n\n        fun move(from: Int, to: Int) {\n            val toMove = proxyList[to - 1]\n            proxyList[to - 1] = proxyList[from - 1]\n            proxyList[from - 1] = toMove\n            notifyItemMoved(from, to)\n            DataStore.dirty = true\n        }\n\n        fun remove(index: Int) {\n            proxyList.removeAt(index - 1)\n            notifyItemRemoved(index)\n            DataStore.dirty = true\n        }\n\n        override fun getItemId(position: Int): Long {\n            return if (position == 0) 0 else proxyList[position - 1].id\n        }\n\n        override fun getItemViewType(position: Int): Int {\n            return if (position == 0) 0 else 1\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {\n            return if (viewType == 0) {\n                AddHolder(LayoutAddEntityBinding.inflate(layoutInflater, parent, false))\n            } else {\n                ProfileHolder(LayoutProfileBinding.inflate(layoutInflater, parent, false))\n            }\n        }\n\n        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {\n            if (holder is AddHolder) {\n                holder.bind()\n            } else if (holder is ProfileHolder) {\n                holder.bind(proxyList[position - 1])\n            }\n        }\n\n        override fun getItemCount(): Int {\n            return proxyList.size + 1\n        }\n\n    }\n\n    fun testProfileAllowed(profile: ProxyEntity): Boolean {\n        if (profile.id == DataStore.editingId) return false\n\n        for (entity in proxyList) {\n            if (testProfileContains(entity, profile)) return false\n        }\n\n        return true\n    }\n\n    fun testProfileContains(profile: ProxyEntity, anotherProfile: ProxyEntity): Boolean {\n        if (profile.type != 8 || anotherProfile.type != 8) return false\n        if (profile.id == anotherProfile.id) return true\n        val proxies = profile.chainBean!!.proxies\n        if (proxies.contains(anotherProfile.id)) return true\n        if (proxies.isNotEmpty()) {\n            for (entity in ProfileManager.getProfiles(proxies)) {\n                if (testProfileContains(entity, anotherProfile)) {\n                    return true\n                }\n            }\n        }\n        return false\n    }\n\n    var replacing = 0\n\n    val selectProfileForAdd =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->\n            if (resultCode == Activity.RESULT_OK) runOnDefaultDispatcher {\n                DataStore.dirty = true\n\n                val profile = ProfileManager.getProfile(\n                    data!!.getLongExtra(\n                        ProfileSelectActivity.EXTRA_PROFILE_ID, 0\n                    )\n                )!!\n\n                if (!testProfileAllowed(profile)) {\n                    onMainDispatcher {\n                        MaterialAlertDialogBuilder(this@BalancerSettingsActivity).setTitle(R.string.circular_reference)\n                            .setMessage(R.string.circular_reference_sum)\n                            .setPositiveButton(android.R.string.ok, null).show()\n                    }\n                } else {\n                    configurationList.post {\n                        if (replacing != 0) {\n                            proxyList[replacing - 1] = profile\n                            configurationAdapter.notifyItemChanged(replacing)\n                        } else {\n                            proxyList.add(profile)\n                            configurationAdapter.notifyItemInserted(proxyList.size)\n                        }\n                    }\n                }\n            }\n        }\n\n    inner class AddHolder(val binding: LayoutAddEntityBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n        fun bind() {\n            binding.root.setOnClickListener {\n                replacing = 0\n                selectProfileForAdd.launch(\n                    Intent(\n                        this@BalancerSettingsActivity, ProfileSelectActivity::class.java\n                    )\n                )\n            }\n        }\n    }\n\n    inner class ProfileHolder(binding: LayoutProfileBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        val profileName = binding.profileName\n        val profileType = binding.profileType\n        val profileAddress = binding.profileAddress\n        val trafficText: TextView = binding.trafficText\n        val selectedView = binding.selectedView\n        val editButton = binding.edit\n        val shareLayout = binding.share\n        val shareLayer = binding.shareLayer\n        val shareButton = binding.shareIcon\n\n        fun bind(proxyEntity: ProxyEntity) {\n\n            profileName.text = proxyEntity.displayName()\n            profileType.text = proxyEntity.displayType()\n\n            var rx = proxyEntity.rx\n            var tx = proxyEntity.tx\n\n            val stats = proxyEntity.stats\n            if (stats != null) {\n                rx += stats.rxTotal\n                tx += stats.txTotal\n            }\n\n            val showTraffic = rx + tx != 0L\n            trafficText.isVisible = showTraffic\n            if (showTraffic) {\n                trafficText.text = itemView.context.getString(\n                    R.string.traffic,\n                    Formatter.formatFileSize(itemView.context, tx),\n                    Formatter.formatFileSize(itemView.context, rx)\n                )\n            }\n\n            editButton.setOnClickListener {\n                replacing = bindingAdapterPosition\n                selectProfileForAdd.launch(Intent(\n                    this@BalancerSettingsActivity, ProfileSelectActivity::class.java\n                ).apply {\n                    putExtra(ProfileSelectActivity.EXTRA_SELECTED, proxyEntity)\n                })\n            }\n\n            shareLayout.isVisible = false\n\n            if (proxyEntity.type != 8) runOnDefaultDispatcher {\n\n                val validateResult = if (DataStore.securityAdvisory) {\n                    proxyEntity.requireBean().isInsecure()\n                } else ResultLocal\n\n                when (validateResult) {\n                    is ResultInsecure -> onMainDispatcher {\n                        shareLayout.isVisible = true\n\n                        shareLayer.setBackgroundColor(Color.RED)\n                        shareButton.setImageResource(R.drawable.ic_baseline_warning_24)\n                        shareButton.setColorFilter(Color.WHITE)\n\n                        shareLayout.setOnClickListener {\n                            MaterialAlertDialogBuilder(this@BalancerSettingsActivity).setTitle(R.string.insecure)\n                                .setMessage(resources.openRawResource(validateResult.textRes)\n                                    .bufferedReader().use { it.readText() })\n                                .setPositiveButton(android.R.string.ok, null).show().apply {\n                                    findViewById<TextView>(android.R.id.message)?.apply {\n                                        Linkify.addLinks(this, Linkify.WEB_URLS)\n                                        movementMethod = LinkMovementMethod.getInstance()\n                                    }\n                                }\n                        }\n                    }\n                    is ResultDeprecated -> onMainDispatcher {\n                        shareLayout.isVisible = true\n\n                        shareLayer.setBackgroundColor(Color.YELLOW)\n                        shareButton.setImageResource(R.drawable.ic_baseline_warning_24)\n                        shareButton.setColorFilter(Color.GRAY)\n\n                        shareLayout.setOnClickListener {\n                            MaterialAlertDialogBuilder(this@BalancerSettingsActivity).setTitle(R.string.deprecated)\n                                .setMessage(resources.openRawResource(validateResult.textRes)\n                                    .bufferedReader().use { it.readText() })\n                                .setPositiveButton(android.R.string.ok, null).show().apply {\n                                    findViewById<TextView>(android.R.id.message)?.apply {\n                                        Linkify.addLinks(this, Linkify.WEB_URLS)\n                                        movementMethod = LinkMovementMethod.getInstance()\n                                    }\n                                }\n                        }\n                    }\n                    else -> onMainDispatcher {\n                        shareLayout.isVisible = false\n                    }\n                }\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/BrookSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.brook.BrookBean\nimport io.nekohasekai.sagernet.ktx.app\n\nclass BrookSettingsActivity : ProfileSettingsActivity<BrookBean>() {\n\n    override fun createEntity() = BrookBean()\n\n    override fun BrookBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverProtocol = protocol\n        DataStore.serverPassword = password\n        DataStore.serverPath = wsPath\n    }\n\n    override fun BrookBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        password = DataStore.serverPassword\n        protocol = DataStore.serverProtocol\n        wsPath = DataStore.serverPath\n    }\n\n    lateinit var protocol: SimpleMenuPreference\n    val protocolValue = app.resources.getStringArray(R.array.brook_protocol_value)\n    lateinit var path: EditTextPreference\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.brook_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n\n        protocol = findPreference(Key.SERVER_PROTOCOL)!!\n        path = findPreference(Key.SERVER_PATH)!!\n\n        if (protocol.value !in protocolValue) {\n            protocol.value = protocolValue[0]\n        }\n        updateProtocol(protocol.value)\n        protocol.setOnPreferenceChangeListener { _, newValue ->\n            updateProtocol(newValue as String)\n            true\n        }\n    }\n\n    fun updateProtocol(value: String) {\n        path.isVisible = value.startsWith(\"ws\")\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/ChainSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Intent\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.text.format.Formatter\nimport android.text.method.LinkMovementMethod\nimport android.text.util.Linkify\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.activity.result.component1\nimport androidx.activity.result.component2\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.core.view.isVisible\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.databinding.LayoutAddEntityBinding\nimport io.nekohasekai.sagernet.databinding.LayoutProfileBinding\nimport io.nekohasekai.sagernet.fmt.internal.ChainBean\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.ui.ProfileSelectActivity\n\nclass ChainSettingsActivity : ProfileSettingsActivity<ChainBean>(R.layout.layout_chain_settings) {\n\n    override fun createEntity() = ChainBean()\n\n    val proxyList = ArrayList<ProxyEntity>()\n\n    override fun ChainBean.init() {\n        DataStore.profileName = name\n        DataStore.serverProtocol = proxies.joinToString(\",\")\n    }\n\n    override fun ChainBean.serialize() {\n        name = DataStore.profileName\n        proxies = proxyList.map { it.id }\n        initializeDefaultValues()\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.name_preferences)\n    }\n\n    lateinit var configurationList: RecyclerView\n    lateinit var configurationAdapter: ProxiesAdapter\n    lateinit var layoutManager: LinearLayoutManager\n\n    @SuppressLint(\"InlinedApi\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        supportActionBar!!.setTitle(R.string.chain_settings)\n        configurationList = findViewById(R.id.configuration_list)\n        layoutManager = FixedLinearLayoutManager(configurationList)\n        configurationList.layoutManager = layoutManager\n        configurationAdapter = ProxiesAdapter()\n        configurationList.adapter = configurationAdapter\n\n        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(\n            ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START\n        ) {\n            override fun getSwipeDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) = if (viewHolder is ProfileHolder) {\n                super.getSwipeDirs(recyclerView, viewHolder)\n            } else 0\n\n            override fun getDragDirs(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n            ) = if (viewHolder is ProfileHolder) {\n                super.getDragDirs(recyclerView, viewHolder)\n            } else 0\n\n            override fun onMove(\n                recyclerView: RecyclerView,\n                viewHolder: RecyclerView.ViewHolder,\n                target: RecyclerView.ViewHolder,\n            ): Boolean {\n                return if (target !is ProfileHolder) false else {\n                    configurationAdapter.move(\n                        viewHolder.bindingAdapterPosition, target.bindingAdapterPosition\n                    )\n                    true\n                }\n            }\n\n            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n                configurationAdapter.remove(viewHolder.bindingAdapterPosition)\n            }\n\n        }).attachToRecyclerView(configurationList)\n    }\n\n    override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n        view.rootView.findViewById<RecyclerView>(R.id.recycler_view).apply {\n            (layoutParams ?: LinearLayout.LayoutParams(-1, -2)).apply {\n                height = -2\n                layoutParams = this\n            }\n        }\n\n        runOnDefaultDispatcher {\n            configurationAdapter.reload()\n        }\n    }\n\n    inner class ProxiesAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {\n\n        suspend fun reload() {\n            val idList = DataStore.serverProtocol.split(\",\")\n                .mapNotNull { it.takeIf { it.isNotBlank() }?.toLong() }\n            if (idList.isNotEmpty()) {\n                val profiles = ProfileManager.getProfiles(idList).map { it.id to it }.toMap()\n                for (id in idList) {\n                    proxyList.add(profiles[id] ?: continue)\n                }\n            }\n            onMainDispatcher {\n                notifyDataSetChanged()\n            }\n        }\n\n        fun move(from: Int, to: Int) {\n            val toMove = proxyList[to - 1]\n            proxyList[to - 1] = proxyList[from - 1]\n            proxyList[from - 1] = toMove\n            notifyItemMoved(from, to)\n            DataStore.dirty = true\n        }\n\n        fun remove(index: Int) {\n            proxyList.removeAt(index - 1)\n            notifyItemRemoved(index)\n            DataStore.dirty = true\n        }\n\n        override fun getItemId(position: Int): Long {\n            return if (position == 0) 0 else proxyList[position - 1].id\n        }\n\n        override fun getItemViewType(position: Int): Int {\n            return if (position == 0) 0 else 1\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {\n            return if (viewType == 0) {\n                AddHolder(LayoutAddEntityBinding.inflate(layoutInflater, parent, false))\n            } else {\n                ProfileHolder(LayoutProfileBinding.inflate(layoutInflater, parent, false))\n            }\n        }\n\n        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {\n            if (holder is AddHolder) {\n                holder.bind()\n            } else if (holder is ProfileHolder) {\n                holder.bind(proxyList[position - 1])\n            }\n        }\n\n        override fun getItemCount(): Int {\n            return proxyList.size + 1\n        }\n\n    }\n\n    fun testProfileAllowed(profile: ProxyEntity): Boolean {\n        if (profile.id == DataStore.editingId) return false\n\n        for (entity in proxyList) {\n            if (testProfileContains(entity, profile)) return false\n        }\n\n        return true\n    }\n\n    fun testProfileContains(profile: ProxyEntity, anotherProfile: ProxyEntity): Boolean {\n        if (profile.type != 8 || anotherProfile.type != 8) return false\n        if (profile.id == anotherProfile.id) return true\n        val proxies = profile.chainBean!!.proxies\n        if (proxies.contains(anotherProfile.id)) return true\n        if (proxies.isNotEmpty()) {\n            for (entity in ProfileManager.getProfiles(proxies)) {\n                if (testProfileContains(entity, anotherProfile)) {\n                    return true\n                }\n            }\n        }\n        return false\n    }\n\n    var replacing = 0\n\n    val selectProfileForAdd =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->\n            if (resultCode == Activity.RESULT_OK) runOnDefaultDispatcher {\n                DataStore.dirty = true\n\n                val profile = ProfileManager.getProfile(\n                    data!!.getLongExtra(\n                        ProfileSelectActivity.EXTRA_PROFILE_ID, 0\n                    )\n                )!!\n\n                if (!testProfileAllowed(profile)) {\n                    onMainDispatcher {\n                        MaterialAlertDialogBuilder(this@ChainSettingsActivity).setTitle(R.string.circular_reference)\n                            .setMessage(R.string.circular_reference_sum)\n                            .setPositiveButton(android.R.string.ok, null).show()\n                    }\n                } else {\n                    configurationList.post {\n                        if (replacing != 0) {\n                            proxyList[replacing - 1] = profile\n                            configurationAdapter.notifyItemChanged(replacing)\n                        } else {\n                            proxyList.add(profile)\n                            configurationAdapter.notifyItemInserted(proxyList.size)\n                        }\n                    }\n                }\n            }\n        }\n\n    inner class AddHolder(val binding: LayoutAddEntityBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n        fun bind() {\n            binding.root.setOnClickListener {\n                replacing = 0\n                selectProfileForAdd.launch(\n                    Intent(\n                        this@ChainSettingsActivity, ProfileSelectActivity::class.java\n                    )\n                )\n            }\n        }\n    }\n\n    inner class ProfileHolder(binding: LayoutProfileBinding) :\n        RecyclerView.ViewHolder(binding.root) {\n\n        val profileName = binding.profileName\n        val profileType = binding.profileType\n        val profileAddress = binding.profileAddress\n        val trafficText: TextView = binding.trafficText\n        val selectedView = binding.selectedView\n        val editButton = binding.edit\n        val shareLayout = binding.share\n        val shareLayer = binding.shareLayer\n        val shareButton = binding.shareIcon\n\n        fun bind(proxyEntity: ProxyEntity) {\n\n            profileName.text = proxyEntity.displayName()\n            profileType.text = proxyEntity.displayType()\n\n            var rx = proxyEntity.rx\n            var tx = proxyEntity.tx\n\n            val stats = proxyEntity.stats\n            if (stats != null) {\n                rx += stats.rxTotal\n                tx += stats.txTotal\n            }\n\n            val showTraffic = rx + tx != 0L\n            trafficText.isVisible = showTraffic\n            if (showTraffic) {\n                trafficText.text = itemView.context.getString(\n                    R.string.traffic,\n                    Formatter.formatFileSize(itemView.context, tx),\n                    Formatter.formatFileSize(itemView.context, rx)\n                )\n            }\n\n            editButton.setOnClickListener {\n                replacing = bindingAdapterPosition\n                selectProfileForAdd.launch(Intent(\n                    this@ChainSettingsActivity, ProfileSelectActivity::class.java\n                ).apply {\n                    putExtra(ProfileSelectActivity.EXTRA_SELECTED, proxyEntity)\n                })\n            }\n\n            shareLayout.isVisible = false\n\n            if (proxyEntity.type != 8) runOnDefaultDispatcher {\n\n                val validateResult = if (DataStore.securityAdvisory) {\n                    proxyEntity.requireBean().isInsecure()\n                } else ResultLocal\n\n                when (validateResult) {\n                    is ResultInsecure -> onMainDispatcher {\n                        shareLayout.isVisible = true\n\n                        shareLayer.setBackgroundColor(Color.RED)\n                        shareButton.setImageResource(R.drawable.ic_baseline_warning_24)\n                        shareButton.setColorFilter(Color.WHITE)\n\n                        shareLayout.setOnClickListener {\n                            MaterialAlertDialogBuilder(this@ChainSettingsActivity).setTitle(R.string.insecure)\n                                .setMessage(resources.openRawResource(validateResult.textRes)\n                                    .bufferedReader().use { it.readText() })\n                                .setPositiveButton(android.R.string.ok, null).show().apply {\n                                    findViewById<TextView>(android.R.id.message)?.apply {\n                                        Linkify.addLinks(this, Linkify.WEB_URLS)\n                                        movementMethod = LinkMovementMethod.getInstance()\n                                    }\n                                }\n                        }\n                    }\n                    is ResultDeprecated -> onMainDispatcher {\n                        shareLayout.isVisible = true\n\n                        shareLayer.setBackgroundColor(Color.YELLOW)\n                        shareButton.setImageResource(R.drawable.ic_baseline_warning_24)\n                        shareButton.setColorFilter(Color.GRAY)\n\n                        shareLayout.setOnClickListener {\n                            MaterialAlertDialogBuilder(this@ChainSettingsActivity).setTitle(R.string.deprecated)\n                                .setMessage(resources.openRawResource(validateResult.textRes)\n                                    .bufferedReader().use { it.readText() })\n                                .setPositiveButton(android.R.string.ok, null).show().apply {\n                                    findViewById<TextView>(android.R.id.message)?.apply {\n                                        Linkify.addLinks(this, Linkify.WEB_URLS)\n                                        movementMethod = LinkMovementMethod.getInstance()\n                                    }\n                                }\n                        }\n                    }\n                    else -> onMainDispatcher {\n                        shareLayout.isVisible = false\n                    }\n                }\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.annotation.SuppressLint\nimport android.content.DialogInterface\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuItem\nimport androidx.appcompat.app.AlertDialog\nimport cn.hutool.json.JSONObject\nimport com.blacksquircle.ui.editorkit.listener.OnChangeListener\nimport com.blacksquircle.ui.editorkit.model.ColorScheme\nimport com.blacksquircle.ui.feature.editor.customview.ExtendedKeyboard\nimport com.blacksquircle.ui.language.base.model.SyntaxScheme\nimport com.blacksquircle.ui.language.json.JsonLanguage\nimport com.github.shadowsocks.plugin.Empty\nimport com.github.shadowsocks.plugin.fragment.AlertDialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.databinding.LayoutEditConfigBinding\nimport io.nekohasekai.sagernet.ktx.getColorAttr\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.ui.ThemedActivity\n\nclass ConfigEditActivity : ThemedActivity() {\n\n    var config = \"\"\n    var dirty = false\n\n    class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.unsaved_changes_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                (requireActivity() as ConfigEditActivity).saveAndExit()\n            }\n            setNegativeButton(R.string.no) { _, _ ->\n                requireActivity().finish()\n            }\n            setNeutralButton(android.R.string.cancel, null)\n        }\n    }\n\n    @SuppressLint(\"InlinedApi\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val binding = LayoutEditConfigBinding.inflate(layoutInflater)\n\n        setContentView(binding.root)\n\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setTitle(R.string.config_settings)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        binding.editor.colorScheme = mkTheme()\n        binding.editor.language = JsonLanguage()\n        binding.editor.onChangeListener = OnChangeListener {\n            config = binding.editor.text.toString()\n            if (!dirty) {\n                dirty = true\n                DataStore.dirty = true\n            }\n        }\n        binding.editor.setHorizontallyScrolling(true)\n        binding.actionTab.setOnClickListener {\n            binding.editor.insert(binding.editor.tab())\n        }\n        val extendedKeyboard = findViewById<ExtendedKeyboard>(R.id.extended_keyboard)\n        extendedKeyboard.setKeyListener { char -> binding.editor.insert(char) }\n        extendedKeyboard.setHasFixedSize(true)\n        extendedKeyboard.submitList(\"{}();,.=|&![]<>+-/*?:_\".map { it.toString() })\n        extendedKeyboard.setBackgroundColor(getColorAttr(R.attr.primaryOrTextPrimary))\n\n        runOnDefaultDispatcher {\n            config = DataStore.serverConfig\n\n            onMainDispatcher {\n                binding.editor.setTextContent(config)\n            }\n        }\n    }\n\n    fun saveAndExit() {\n        config = try {\n            JSONObject(config).toStringPretty()\n        } catch (e: Exception) {\n            MaterialAlertDialogBuilder(this).setTitle(R.string.error_title)\n                .setMessage(e.readableMessage).show()\n            return\n        }\n\n        DataStore.serverConfig = config\n        finish()\n    }\n\n    override fun onBackPressed() {\n        if (dirty) UnsavedChangesDialogFragment().apply { key() }\n            .show(supportFragmentManager, null) else super.onBackPressed()\n    }\n\n\n    override fun onSupportNavigateUp(): Boolean {\n        if (!super.onSupportNavigateUp()) finish()\n        return true\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.profile_apply_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        when (item.itemId) {\n            R.id.action_apply -> {\n                saveAndExit()\n                return true\n            }\n        }\n        return super.onOptionsItemSelected(item)\n\n    }\n\n    fun mkTheme(): ColorScheme {\n        val colorPrimary = getColorAttr(R.attr.colorPrimary)\n        val colorPrimaryDark = getColorAttr(R.attr.colorPrimaryDark)\n\n        return ColorScheme(\n            textColor = colorPrimary,\n            backgroundColor = Color.WHITE,\n            gutterColor = colorPrimary,\n            gutterDividerColor = Color.WHITE,\n            gutterCurrentLineNumberColor = Color.WHITE,\n            gutterTextColor = Color.WHITE,\n            selectedLineColor = Color.parseColor(\"#D3D3D3\"),\n            selectionColor = colorPrimary,\n            suggestionQueryColor = Color.parseColor(\"#7CE0F3\"),\n            findResultBackgroundColor = Color.parseColor(\"#5F5E5A\"),\n            delimiterBackgroundColor = Color.parseColor(\"#5F5E5A\"),\n            syntaxScheme = SyntaxScheme(\n                numberColor = Color.parseColor(\"#BB8FF8\"),\n                operatorColor = Color.BLACK,\n                keywordColor = Color.parseColor(\"#EB347E\"),\n                typeColor = Color.parseColor(\"#7FD0E4\"),\n                langConstColor = Color.parseColor(\"#EB347E\"),\n                preprocessorColor = Color.parseColor(\"#EB347E\"),\n                variableColor = Color.parseColor(\"#7FD0E4\"),\n                methodColor = Color.parseColor(\"#B6E951\"),\n                stringColor = colorPrimaryDark,\n                commentColor = Color.parseColor(\"#89826D\"),\n                tagColor = Color.parseColor(\"#F8F8F8\"),\n                tagNameColor = Color.parseColor(\"#EB347E\"),\n                attrNameColor = Color.parseColor(\"#B6E951\"),\n                attrValueColor = Color.parseColor(\"#EBE48C\"),\n                entityRefColor = Color.parseColor(\"#BB8FF8\")\n            )\n        )\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.internal.ConfigBean\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.widget.EditConfigPreference\n\nclass ConfigSettingsActivity : ProfileSettingsActivity<ConfigBean>() {\n\n    override fun createEntity() =\n        ConfigBean()\n\n    var config = \"\"\n    var dirty = false\n\n    override fun ConfigBean.init() {\n        DataStore.profileName = name\n        DataStore.serverProtocol = type\n        DataStore.serverConfig = content\n        config = content\n    }\n\n    override fun ConfigBean.serialize() {\n        name = DataStore.profileName\n        type = DataStore.serverProtocol\n        content = config\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        supportActionBar!!.setTitle(R.string.config_settings)\n    }\n\n    lateinit var editConfigPreference: EditConfigPreference\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.config_preferences)\n        editConfigPreference = findPreference(Key.SERVER_CONFIG)!!\n    }\n\n    override fun onResume() {\n        super.onResume()\n\n        if (::editConfigPreference.isInitialized) {\n            runOnDefaultDispatcher {\n                val newConfig = DataStore.serverConfig\n\n                if (newConfig != config) {\n                    config = newConfig\n\n                    onMainDispatcher {\n                        editConfigPreference.notifyChanged()\n                    }\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/HttpSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.http.HttpBean\n\nclass HttpSettingsActivity : ProfileSettingsActivity<HttpBean>() {\n\n    override fun createEntity() = HttpBean()\n\n    override fun HttpBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverUsername = username\n        DataStore.serverPassword = password\n        DataStore.serverTLS = tls\n        DataStore.serverSNI = sni\n    }\n\n    override fun HttpBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        username = DataStore.serverUsername\n        password = DataStore.serverPassword\n        tls = DataStore.serverTLS\n        sni = DataStore.serverSNI\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.http_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\n\nclass HysteriaSettingsActivity : ProfileSettingsActivity<HysteriaBean>() {\n\n    override fun createEntity() = HysteriaBean().applyDefaultValues()\n\n    override fun HysteriaBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverObfs = obfuscation\n        DataStore.serverAuthType = authPayloadType\n        DataStore.serverPassword = authPayload\n        DataStore.serverSNI = sni\n        DataStore.serverCertificates = caText\n        DataStore.serverAllowInsecure = allowInsecure\n        DataStore.serverUploadSpeed = uploadMbps\n        DataStore.serverDownloadSpeed = downloadMbps\n        DataStore.serverStreamReceiveWindow = streamReceiveWindow\n        DataStore.serverConnectionReceiveWindow = connectionReceiveWindow\n        DataStore.serverDisableMtuDiscovery = disableMtuDiscovery\n    }\n\n    override fun HysteriaBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        obfuscation = DataStore.serverObfs\n        authPayloadType = DataStore.serverAuthType\n        authPayload = DataStore.serverPassword\n        sni = DataStore.serverSNI\n        caText = DataStore.serverCertificates\n        allowInsecure = DataStore.serverAllowInsecure\n        uploadMbps = DataStore.serverUploadSpeed\n        downloadMbps = DataStore.serverDownloadSpeed\n        streamReceiveWindow = DataStore.serverStreamReceiveWindow\n        connectionReceiveWindow = DataStore.serverConnectionReceiveWindow\n        disableMtuDiscovery = DataStore.serverDisableMtuDiscovery\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.hysteria_preferences)\n\n        val authType = findPreference<SimpleMenuPreference>(Key.SERVER_AUTH_TYPE)!!\n        val authPayload = findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!\n        authPayload.isVisible = authType.value != \"${HysteriaBean.TYPE_NONE}\"\n        authType.setOnPreferenceChangeListener { _, newValue ->\n            authPayload.isVisible = newValue != \"${HysteriaBean.TYPE_NONE}\"\n            true\n        }\n\n        findPreference<EditTextPreference>(Key.SERVER_UPLOAD_SPEED)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Number)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_DOWNLOAD_SPEED)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Number)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_STREAM_RECEIVE_WINDOW)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Number)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_CONNECTION_RECEIVE_WINDOW)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Number)\n        }\n\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/NaiveSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.naive.NaiveBean\n\nclass NaiveSettingsActivity : ProfileSettingsActivity<NaiveBean>() {\n\n    override fun createEntity() = NaiveBean()\n\n    override fun NaiveBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverUsername = username\n        DataStore.serverPassword = password\n        DataStore.serverProtocol = proto\n        DataStore.serverHeaders = extraHeaders\n    }\n\n    override fun NaiveBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        username = DataStore.serverUsername\n        password = DataStore.serverPassword\n        proto = DataStore.serverProtocol\n        extraHeaders = DataStore.serverHeaders.replace(\"\\r\\n\", \"\\n\")\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.naive_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/PingTunnelSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport cn.hutool.core.util.NumberUtil\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.pingtunnel.PingTunnelBean\n\nclass PingTunnelSettingsActivity : ProfileSettingsActivity<PingTunnelBean>() {\n\n    override fun createEntity() = PingTunnelBean()\n\n    override fun PingTunnelBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPassword = key\n    }\n\n    override fun PingTunnelBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        key = DataStore.serverPassword\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.pingtunnel_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Number)\n            setOnPreferenceChangeListener { _, newValue ->\n                newValue.toString().let {\n                    it.isBlank() || NumberUtil.isInteger(it)\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport androidx.annotation.LayoutRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.view.ViewCompat\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceDataStore\nimport com.github.shadowsocks.plugin.Empty\nimport com.github.shadowsocks.plugin.fragment.AlertDialogFragment\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.database.SagerDatabase\nimport io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener\nimport io.nekohasekai.sagernet.fmt.AbstractBean\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport io.nekohasekai.sagernet.ktx.onMainDispatcher\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.ui.ThemedActivity\nimport io.nekohasekai.sagernet.utils.DirectBoot\nimport io.nekohasekai.sagernet.widget.ListListener\nimport kotlinx.parcelize.Parcelize\nimport kotlin.properties.Delegates\n\n@Suppress(\"UNCHECKED_CAST\")\nabstract class ProfileSettingsActivity<T : AbstractBean>(\n    @LayoutRes resId: Int = R.layout.layout_config_settings,\n) : ThemedActivity(resId),\n    OnPreferenceDataStoreChangeListener {\n\n    class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.unsaved_changes_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                runOnDefaultDispatcher {\n                    (requireActivity() as ProfileSettingsActivity<*>).saveAndExit()\n                }\n            }\n            setNegativeButton(R.string.no) { _, _ ->\n                requireActivity().finish()\n            }\n            setNeutralButton(android.R.string.cancel, null)\n        }\n    }\n\n    @Parcelize\n    data class ProfileIdArg(val profileId: Long, val groupId: Long) : Parcelable\n    class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {\n        override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {\n            setTitle(R.string.delete_confirm_prompt)\n            setPositiveButton(R.string.yes) { _, _ ->\n                runOnDefaultDispatcher {\n                    ProfileManager.deleteProfile(arg.groupId, arg.profileId)\n                }\n                requireActivity().finish()\n            }\n            setNegativeButton(R.string.no, null)\n        }\n    }\n\n    companion object {\n        const val EXTRA_PROFILE_ID = \"id\"\n        const val EXTRA_IS_SUBSCRIPTION = \"sub\"\n    }\n\n    abstract fun createEntity(): T\n    abstract fun T.init()\n    abstract fun T.serialize()\n\n    protected var isSubscription by Delegates.notNull<Boolean>()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setSupportActionBar(findViewById(R.id.toolbar))\n        supportActionBar?.apply {\n            setTitle(R.string.profile_config)\n            setDisplayHomeAsUpEnabled(true)\n            setHomeAsUpIndicator(R.drawable.ic_navigation_close)\n        }\n\n        if (savedInstanceState == null) {\n            val editingId = intent.getLongExtra(EXTRA_PROFILE_ID, 0L)\n            isSubscription = intent.getBooleanExtra(EXTRA_IS_SUBSCRIPTION, false)\n            DataStore.editingId = editingId\n            runOnDefaultDispatcher {\n                if (editingId == 0L) {\n                    DataStore.editingGroup = DataStore.selectedGroupForImport()\n                    createEntity().applyDefaultValues().init()\n                } else {\n                    val proxyEntity = SagerDatabase.proxyDao.getById(editingId)\n                    if (proxyEntity == null) {\n                        onMainDispatcher {\n                            finish()\n                        }\n                        return@runOnDefaultDispatcher\n                    }\n                    DataStore.editingGroup = proxyEntity.groupId\n                    (proxyEntity.requireBean() as T).init()\n                }\n\n                onMainDispatcher {\n                    supportFragmentManager\n                        .beginTransaction()\n                        .replace(R.id.settings, MyPreferenceFragmentCompat().apply {\n                            activity = this@ProfileSettingsActivity\n                        })\n                        .commit()\n                }\n            }\n\n\n        }\n\n    }\n\n    open suspend fun saveAndExit() {\n\n        val editingId = DataStore.editingId\n        if (editingId == 0L) {\n            val editingGroup = DataStore.editingGroup\n            ProfileManager.createProfile(editingGroup, createEntity().apply { serialize() })\n        } else {\n            val entity = SagerDatabase.proxyDao.getById(DataStore.editingId)\n            if (entity == null) {\n                finish()\n                return\n            }\n            if (entity.id == DataStore.selectedProxy) {\n                SagerNet.stopService()\n            }\n            ProfileManager.updateProfile(entity.apply { (requireBean() as T).serialize() })\n        }\n        if (editingId == DataStore.selectedProxy && DataStore.directBootAware) DirectBoot.update()\n        finish()\n\n    }\n\n    val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat }\n\n    override fun onCreateOptionsMenu(menu: Menu?): Boolean {\n        menuInflater.inflate(R.menu.profile_config_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)\n\n    override fun onBackPressed() {\n        if (DataStore.dirty) UnsavedChangesDialogFragment()\n            .apply { key() }\n            .show(supportFragmentManager, null) else super.onBackPressed()\n    }\n\n    override fun onSupportNavigateUp(): Boolean {\n        if (!super.onSupportNavigateUp()) finish()\n        return true\n    }\n\n    override fun onDestroy() {\n        DataStore.profileCacheStore.unregisterChangeListener(this)\n        super.onDestroy()\n    }\n\n    override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {\n        if (key != Key.PROFILE_DIRTY) {\n            DataStore.dirty = true\n        }\n    }\n\n    abstract fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    )\n\n    open fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n    }\n\n    open fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {\n        return false\n    }\n\n    class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {\n\n        lateinit var activity: ProfileSettingsActivity<*>\n\n        override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {\n            preferenceManager.preferenceDataStore = DataStore.profileCacheStore\n            activity.apply {\n                createPreferences(savedInstanceState, rootKey)\n\n                if (isSubscription) {\n//                    findPreference<Preference>(Key.PROFILE_NAME)?.isEnabled = false\n                }\n            }\n        }\n\n        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n            super.onViewCreated(view, savedInstanceState)\n\n            ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)\n\n            activity.apply {\n                viewCreated(view, savedInstanceState)\n            }\n\n            DataStore.dirty = false\n            DataStore.profileCacheStore.registerChangeListener(activity)\n        }\n\n        override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {\n            R.id.action_delete -> {\n                if (DataStore.editingId == 0L) {\n                    requireActivity().finish()\n                } else {\n                    DeleteConfirmationDialogFragment().apply {\n                        arg(\n                            ProfileIdArg(\n                                DataStore.editingId, DataStore.editingGroup\n                            )\n                        )\n                        key()\n                    }.show(parentFragmentManager, null)\n                }\n                true\n            }\n            R.id.action_apply -> {\n                runOnDefaultDispatcher {\n                    activity.saveAndExit()\n                }\n                true\n            }\n            else -> false\n        }\n\n        override fun onDisplayPreferenceDialog(preference: Preference) {\n            activity.apply {\n                if (displayPreferenceDialog(preference)) return\n            }\n            super.onDisplayPreferenceDialog(preference)\n        }\n\n    }\n\n    object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {\n\n        override fun provideSummary(preference: EditTextPreference): CharSequence {\n            return if (preference.text.isNullOrBlank()) {\n                preference.context.getString(androidx.preference.R.string.not_set)\n            } else {\n                \"\\u2022\".repeat(preference.text.length)\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/RelayBatonSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.fmt.relaybaton.RelayBatonBean\n\nclass RelayBatonSettingsActivity : ProfileSettingsActivity<RelayBatonBean>() {\n\n    override fun createEntity() = RelayBatonBean()\n\n    override fun RelayBatonBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverUsername = username\n        DataStore.serverPassword = password\n    }\n\n    override fun RelayBatonBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        username = DataStore.serverUsername\n        password = DataStore.serverPassword\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.relaybaton_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/SSHSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.ssh.SSHBean\n\nclass SSHSettingsActivity : ProfileSettingsActivity<SSHBean>() {\n\n    override fun createEntity() = SSHBean()\n\n    override fun SSHBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverUsername = username\n        DataStore.serverAuthType = authType\n        DataStore.serverPassword = password\n        DataStore.serverPrivateKey = privateKey\n        DataStore.serverPassword1 = privateKeyPassphrase\n        DataStore.serverCertificates = publicKey\n    }\n\n    override fun SSHBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        username = DataStore.serverUsername\n        authType = DataStore.serverAuthType\n        when (authType) {\n            SSHBean.AUTH_TYPE_NONE -> {\n            }\n            SSHBean.AUTH_TYPE_PASSWORD -> {\n                password = DataStore.serverPassword\n            }\n            SSHBean.AUTH_TYPE_PRIVATE_KEY -> {\n                privateKey = DataStore.serverPrivateKey\n                privateKeyPassphrase = DataStore.serverPassword1\n            }\n        }\n        publicKey = DataStore.serverCertificates\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.ssh_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        val password = findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n        val privateKey = findPreference<EditTextPreference>(Key.SERVER_PRIVATE_KEY)!!\n        val privateKeyPassphrase = findPreference<EditTextPreference>(Key.SERVER_PASSWORD1)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n        val authType = findPreference<SimpleMenuPreference>(Key.SERVER_AUTH_TYPE)!!\n        fun updateAuthType(type: Int = DataStore.serverAuthType) {\n            password.isVisible = type == SSHBean.AUTH_TYPE_PASSWORD\n            privateKey.isVisible = type == SSHBean.AUTH_TYPE_PRIVATE_KEY\n            privateKeyPassphrase.isVisible = type == SSHBean.AUTH_TYPE_PRIVATE_KEY\n        }\n        updateAuthType()\n        authType.setOnPreferenceChangeListener { _, newValue ->\n            updateAuthType((newValue as String).toInt())\n            true\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksRSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean\n\nclass ShadowsocksRSettingsActivity : ProfileSettingsActivity<ShadowsocksRBean>() {\n\n    override fun createEntity() = ShadowsocksRBean()\n\n    override fun ShadowsocksRBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverPassword = password\n        DataStore.serverMethod = method\n        DataStore.serverProtocol = protocol\n        DataStore.serverProtocolParam = protocolParam\n        DataStore.serverObfs = obfs\n        DataStore.serverObfsParam = obfsParam\n    }\n\n    override fun ShadowsocksRBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        method = DataStore.serverMethod\n        password = DataStore.serverPassword\n        protocol = DataStore.serverProtocol\n        protocolParam = DataStore.serverProtocolParam\n        obfs = DataStore.serverObfs\n        obfsParam = DataStore.serverObfsParam\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.shadowsocksr_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/ShadowsocksSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.app.Activity\nimport android.content.BroadcastReceiver\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.view.View\nimport androidx.activity.result.component1\nimport androidx.activity.result.component2\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.fragment.app.setFragmentResultListener\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.whenCreated\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport com.github.shadowsocks.plugin.*\nimport com.github.shadowsocks.plugin.fragment.AlertDialogFragment\nimport com.github.shadowsocks.preference.PluginConfigurationDialogFragment\nimport com.github.shadowsocks.preference.PluginPreference\nimport com.github.shadowsocks.preference.PluginPreferenceDialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.snackbar.Snackbar\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean\nimport io.nekohasekai.sagernet.ktx.listenForPackageChanges\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher\nimport io.nekohasekai.sagernet.ktx.showAllowingStateLoss\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass ShadowsocksSettingsActivity : ProfileSettingsActivity<ShadowsocksBean>(),\n    Preference.OnPreferenceChangeListener {\n\n    override fun createEntity() = ShadowsocksBean()\n\n    private lateinit var plugin: PluginPreference\n    private lateinit var pluginConfigure: EditTextPreference\n    private lateinit var pluginConfiguration: PluginConfiguration\n    private lateinit var receiver: BroadcastReceiver\n\n    override fun ShadowsocksBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverMethod = method\n        DataStore.serverPassword = password\n        DataStore.serverPlugin = plugin\n    }\n\n    override fun ShadowsocksBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        method = DataStore.serverMethod\n        password = DataStore.serverPassword\n        plugin = DataStore.serverPlugin\n\n    }\n\n    override fun onAttachedToWindow() {\n        super.onAttachedToWindow()\n\n        receiver = listenForPackageChanges(false) {\n            lifecycleScope.launch(Dispatchers.Main) {   // wait until changes were flushed\n                whenCreated { initPlugins() }\n            }\n        }\n    }\n\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.shadowsocks_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n\n        plugin = findPreference(Key.SERVER_PLUGIN)!!\n        pluginConfigure = findPreference(Key.SERVER_PLUGIN_CONFIGURE)!!\n        pluginConfigure.setOnBindEditTextListener(EditTextPreferenceModifiers.Monospace)\n        pluginConfigure.onPreferenceChangeListener = this@ShadowsocksSettingsActivity\n        pluginConfiguration = PluginConfiguration(DataStore.serverPlugin ?: \"\")\n        initPlugins()\n    }\n\n    override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n        setFragmentResultListener(PluginPreferenceDialogFragment::class.java.name) { _, bundle ->\n            val selected = plugin.plugins.lookup.getValue(\n                bundle.getString(PluginPreferenceDialogFragment.KEY_SELECTED_ID)!!)\n            val override = pluginConfiguration.pluginsOptions.keys.firstOrNull {\n                plugin.plugins.lookup[it] == selected\n            }\n            pluginConfiguration =\n                PluginConfiguration(pluginConfiguration.pluginsOptions, override ?: selected.id)\n            DataStore.serverPlugin = pluginConfiguration.toString()\n            DataStore.dirty = true\n            plugin.value = pluginConfiguration.selected\n            pluginConfigure.isEnabled = selected !is NoPlugin\n            pluginConfigure.text = pluginConfiguration.getOptions().toString()\n            if (!selected.trusted) {\n                Snackbar.make(requireView(), R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()\n            }\n        }\n        AlertDialogFragment.setResultListener<Empty>(this,\n            UnsavedChangesDialogFragment::class.java.simpleName) { which, _ ->\n            when (which) {\n                DialogInterface.BUTTON_POSITIVE -> {\n                    runOnDefaultDispatcher {\n                        saveAndExit()\n                    }\n                }\n                DialogInterface.BUTTON_NEGATIVE -> requireActivity().finish()\n            }\n        }\n    }\n\n    private fun initPlugins() {\n        plugin.value = pluginConfiguration.selected\n        plugin.init()\n        pluginConfigure.isEnabled = plugin.selectedEntry?.let { it is NoPlugin } == false\n        pluginConfigure.text = pluginConfiguration.getOptions().toString()\n    }\n\n    private fun showPluginEditor() {\n        PluginConfigurationDialogFragment().apply {\n            setArg(Key.SERVER_PLUGIN_CONFIGURE, pluginConfiguration.selected)\n            setTargetFragment(child, 0)\n        }.showAllowingStateLoss(supportFragmentManager, Key.SERVER_PLUGIN_CONFIGURE)\n    }\n\n    override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean = try {\n        val selected = pluginConfiguration.selected\n        pluginConfiguration = PluginConfiguration((pluginConfiguration.pluginsOptions +\n                (pluginConfiguration.selected to PluginOptions(selected,\n                    newValue as? String?))).toMutableMap(),\n            selected)\n        DataStore.serverPlugin = pluginConfiguration.toString()\n        DataStore.dirty = true\n        true\n    } catch (exc: RuntimeException) {\n        Snackbar.make(child.requireView(), exc.readableMessage, Snackbar.LENGTH_LONG).show()\n        false\n    }\n\n    private val configurePlugin =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->\n            when (resultCode) {\n                Activity.RESULT_OK -> {\n                    val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)\n                    pluginConfigure.text = options\n                    onPreferenceChange(null, options)\n                }\n                PluginContract.RESULT_FALLBACK -> showPluginEditor()\n            }\n        }\n\n    override fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {\n        when (preference.key) {\n            Key.SERVER_PLUGIN -> PluginPreferenceDialogFragment().apply {\n                setArg(Key.SERVER_PLUGIN)\n                setTargetFragment(child, 0)\n            }.showAllowingStateLoss(supportFragmentManager, Key.SERVER_PLUGIN)\n            Key.SERVER_PLUGIN_CONFIGURE -> {\n                val intent = PluginManager.buildIntent(plugin.selectedEntry!!.id,\n                    PluginContract.ACTION_CONFIGURE)\n                if (intent.resolveActivity(packageManager) == null) showPluginEditor() else {\n                    configurePlugin.launch(intent\n                        .putExtra(PluginContract.EXTRA_OPTIONS,\n                            pluginConfiguration.getOptions().toString()))\n                }\n            }\n            else -> return false\n        }\n        return true\n    }\n\n    val pluginHelp = registerForActivityResult(\n        ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->\n        if (resultCode == Activity.RESULT_OK) MaterialAlertDialogBuilder(this)\n            .setTitle(\"?\")\n            .setMessage(data?.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))\n            .show()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.snell.SnellBean\n\nclass SnellSettingsActivity : ProfileSettingsActivity<SnellBean>() {\n\n    override fun createEntity() = SnellBean()\n\n    override fun SnellBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverPassword = psk\n        DataStore.serverProtocolVersion = version\n        DataStore.serverObfs = obfsMode\n        DataStore.serverObfsParam = obfsHost\n    }\n\n    override fun SnellBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        psk = DataStore.serverPassword\n        version = DataStore.serverProtocolVersion\n        obfsMode = DataStore.serverObfs\n        obfsHost = DataStore.serverObfsParam\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.snell_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/SocksSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport androidx.preference.SwitchPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.socks.SOCKSBean\n\nclass SocksSettingsActivity : ProfileSettingsActivity<SOCKSBean>() {\n\n    override fun createEntity() = SOCKSBean()\n\n    override fun SOCKSBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n\n        DataStore.serverProtocolVersion = protocol\n        DataStore.serverUsername = username\n        DataStore.serverPassword = password\n        DataStore.serverTLS = tls\n        DataStore.serverSNI = sni\n    }\n\n    override fun SOCKSBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n\n        protocol = DataStore.serverProtocolVersion\n        username = DataStore.serverUsername\n        password = DataStore.serverPassword\n        tls = DataStore.serverTLS\n        sni = DataStore.serverSNI\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.socks_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        val password = findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n        val protocol = findPreference<SimpleMenuPreference>(Key.SERVER_PROTOCOL)!!\n        val useTls = findPreference<SwitchPreference>(Key.SERVER_TLS)!!\n        val sni = findPreference<EditTextPreference>(Key.SERVER_SNI)!!\n\n        fun updateProtocol(version: Int) {\n            password.isVisible = version == SOCKSBean.PROTOCOL_SOCKS5\n            useTls.isVisible = version == SOCKSBean.PROTOCOL_SOCKS5\n            sni.isVisible = version == SOCKSBean.PROTOCOL_SOCKS5\n        }\n\n        updateProtocol(DataStore.serverProtocolVersion)\n        protocol.setOnPreferenceChangeListener { _, newValue ->\n            updateProtocol((newValue as String).toInt())\n            true\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/StandardV2RaySettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport androidx.preference.PreferenceCategory\nimport androidx.preference.SwitchPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean\nimport io.nekohasekai.sagernet.ktx.app\n\nabstract class StandardV2RaySettingsActivity : ProfileSettingsActivity<StandardV2RayBean>() {\n\n    var bean: StandardV2RayBean? = null\n\n    override fun StandardV2RayBean.init() {\n        bean = this\n\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverUserId = uuid\n        DataStore.serverEncryption = encryption\n        if (this is VMessBean) {\n            DataStore.serverAlterId = alterId\n        }\n        DataStore.serverNetwork = type\n        DataStore.serverHeader = headerType\n        DataStore.serverHost = host\n\n        when (type) {\n            \"kcp\" -> DataStore.serverPath = mKcpSeed\n            \"quic\" -> DataStore.serverPath = quicKey\n            \"grpc\" -> {\n                DataStore.serverPath = grpcServiceName\n                DataStore.serverMultiMode = grpcMultiMode\n            }\n            else -> DataStore.serverPath = path\n        }\n\n        DataStore.serverSecurity = security\n        DataStore.serverSNI = sni\n        DataStore.serverALPN = alpn\n        DataStore.serverCertificates = certificates\n        DataStore.serverFlow = flow\n        DataStore.serverQuicSecurity = quicSecurity\n\n        DataStore.serverWsBrowserForwarding = wsUseBrowserForwarder\n        DataStore.serverAllowInsecure = allowInsecure\n\n    }\n\n    override fun StandardV2RayBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        uuid = DataStore.serverUserId\n        encryption = DataStore.serverEncryption\n        if (this is VMessBean) {\n            alterId = DataStore.serverAlterId\n        }\n        type = DataStore.serverNetwork\n        headerType = DataStore.serverHeader\n        host = DataStore.serverHost\n        when (type) {\n            \"kcp\" -> mKcpSeed = DataStore.serverPath\n            \"quic\" -> quicKey = DataStore.serverPath\n            \"grpc\" -> {\n                grpcServiceName = DataStore.serverPath\n                grpcMultiMode = DataStore.serverMultiMode\n            }\n            else -> path = DataStore.serverPath\n        }\n        security = DataStore.serverSecurity\n        sni = DataStore.serverSNI\n        alpn = DataStore.serverALPN\n        certificates = DataStore.serverCertificates\n        flow = DataStore.serverFlow\n        quicSecurity = DataStore.serverQuicSecurity\n\n        wsUseBrowserForwarder = DataStore.serverWsBrowserForwarding\n        allowInsecure = DataStore.serverAllowInsecure\n    }\n\n    lateinit var encryption: SimpleMenuPreference\n    lateinit var network: SimpleMenuPreference\n    lateinit var header: SimpleMenuPreference\n    lateinit var requestHost: EditTextPreference\n    lateinit var path: EditTextPreference\n    lateinit var quicSecurity: SimpleMenuPreference\n    lateinit var security: SimpleMenuPreference\n    lateinit var multiMode: SwitchPreference\n    lateinit var xtlsFlow: SimpleMenuPreference\n\n    lateinit var securityCategory: PreferenceCategory\n    lateinit var wsCategory: PreferenceCategory\n    lateinit var vmessExperimentsCategory: PreferenceCategory\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.standard_v2ray_preferences)\n\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n\n        encryption = findPreference(Key.SERVER_ENCRYPTION)!!\n        network = findPreference(Key.SERVER_NETWORK)!!\n        header = findPreference(Key.SERVER_HEADER)!!\n        requestHost = findPreference(Key.SERVER_HOST)!!\n        path = findPreference(Key.SERVER_PATH)!!\n        quicSecurity = findPreference(Key.SERVER_QUIC_SECURITY)!!\n        security = findPreference(Key.SERVER_SECURITY)!!\n\n        securityCategory = findPreference(Key.SERVER_SECURITY_CATEGORY)!!\n        wsCategory = findPreference(Key.SERVER_WS_CATEGORY)!!\n\n        xtlsFlow = findPreference(Key.SERVER_FLOW)!!\n        multiMode = findPreference(Key.SERVER_MULTI_MODE)!!\n\n        val alterId = findPreference<EditTextPreference>(Key.SERVER_ALTER_ID)!!\n        if (bean is VLESSBean) {\n            alterId.isVisible = false\n\n            encryption.setEntries(R.array.vless_encryption_entry)\n            encryption.setEntryValues(R.array.vless_encryption_value)\n\n            val vev = resources.getStringArray(R.array.vless_encryption_value)\n            if (encryption.value !in vev) {\n                encryption.value = vev[0]\n            }\n        } else {\n            alterId.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n\n            encryption.setEntries(R.array.vmess_encryption_entry)\n            encryption.setEntryValues(R.array.vmess_encryption_value)\n\n            val vev = resources.getStringArray(R.array.vmess_encryption_value)\n            if (encryption.value !in vev) {\n                encryption.value = \"auto\"\n            }\n        }\n\n        findPreference<EditTextPreference>(Key.SERVER_USER_ID)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n\n        updateView(network.value)\n        network.setOnPreferenceChangeListener { _, newValue ->\n            updateView(newValue as String)\n            true\n        }\n\n        security.setOnPreferenceChangeListener { _, newValue ->\n            updateTle(newValue as String)\n            true\n        }\n\n        vmessExperimentsCategory = findPreference(Key.SERVER_VMESS_EXPERIMENTS_CATEGORY)!!\n        vmessExperimentsCategory.isVisible = false\n    }\n\n    val tcpHeadersValue = app.resources.getStringArray(R.array.tcp_headers_value)\n    val kcpQuicHeadersValue = app.resources.getStringArray(R.array.kcp_quic_headers_value)\n    val quicSecurityValue = app.resources.getStringArray(R.array.quic_security_value)\n    val xtlsFlowValue = app.resources.getStringArray(R.array.xtls_flow_value)\n\n    fun updateView(network: String) {\n        if (bean is StandardV2RayBean) {\n            when (network) {\n                \"tcp\", \"kcp\" -> {\n                    security.setEntries(R.array.transport_layer_encryption_entry)\n                    security.setEntryValues(R.array.transport_layer_encryption_value)\n                    security.value = DataStore.serverSecurity\n\n                    val tlev =\n                        resources.getStringArray(R.array.transport_layer_encryption_value)\n                    if (security.value !in tlev) {\n                        security.value = tlev[0]\n                    }\n                }\n                else -> {\n                    security.setEntries(R.array.transport_layer_encryption_entry)\n                    security.setEntryValues(R.array.transport_layer_encryption_value)\n                    security.value = DataStore.serverSecurity\n\n                    val tlev = resources.getStringArray(R.array.transport_layer_encryption_value)\n                    if (security.value !in tlev) {\n                        security.value = tlev[0]\n                    }\n                }\n            }\n        }\n\n        updateTle(security.value)\n\n        val isQuic = network == \"quic\"\n        val isGRPC = network == \"grpc\"\n        val isWs = network == \"ws\"\n        quicSecurity.isVisible = isQuic\n        if (isQuic) {\n            if (DataStore.serverQuicSecurity !in quicSecurityValue) {\n                quicSecurity.value = quicSecurityValue[0]\n            } else {\n                quicSecurity.value = DataStore.serverQuicSecurity\n            }\n        }\n        multiMode.isVisible = isGRPC\n        wsCategory.isVisible = isWs\n\n        when (network) {\n            \"tcp\" -> {\n                header.setEntries(R.array.tcp_headers_entry)\n                header.setEntryValues(R.array.tcp_headers_value)\n\n                if (DataStore.serverHeader !in tcpHeadersValue) {\n                    header.value = tcpHeadersValue[0]\n                } else {\n                    header.value = DataStore.serverHeader\n                }\n\n                var isHttp = header.value == \"http\"\n                requestHost.isVisible = isHttp\n                path.isVisible = isHttp\n\n                header.setOnPreferenceChangeListener { _, newValue ->\n                    isHttp = newValue == \"http\"\n                    requestHost.isVisible = isHttp\n                    path.isVisible = isHttp\n                    true\n                }\n\n                requestHost.setTitle(R.string.http_host)\n                path.setTitle(R.string.http_path)\n                header.isVisible = true\n            }\n            \"http\" -> {\n                requestHost.setTitle(R.string.http_host)\n                path.setTitle(R.string.http_path)\n\n                header.isVisible = false\n                requestHost.isVisible = true\n                path.isVisible = true\n            }\n            \"ws\" -> {\n                requestHost.setTitle(R.string.ws_host)\n                path.setTitle(R.string.ws_path)\n\n                header.isVisible = false\n                requestHost.isVisible = true\n                path.isVisible = true\n            }\n            \"kcp\" -> {\n                header.setEntries(R.array.kcp_quic_headers_entry)\n                header.setEntryValues(R.array.kcp_quic_headers_value)\n                path.setTitle(R.string.kcp_seed)\n\n                if (DataStore.serverHeader !in kcpQuicHeadersValue) {\n                    header.value = kcpQuicHeadersValue[0]\n                } else {\n                    header.value = DataStore.serverHeader\n                }\n\n                header.onPreferenceChangeListener = null\n\n                header.isVisible = true\n                requestHost.isVisible = false\n                path.isVisible = true\n            }\n            \"quic\" -> {\n                header.setEntries(R.array.kcp_quic_headers_entry)\n                header.setEntryValues(R.array.kcp_quic_headers_value)\n                path.setTitle(R.string.quic_key)\n\n                if (DataStore.serverHeader !in kcpQuicHeadersValue) {\n                    header.value = kcpQuicHeadersValue[0]\n                } else {\n                    header.value = DataStore.serverHeader\n                }\n\n                header.onPreferenceChangeListener = null\n\n                header.isVisible = true\n                requestHost.isVisible = false\n                path.isVisible = true\n            }\n            \"grpc\" -> {\n                path.setTitle(R.string.grpc_service_name)\n\n                header.isVisible = false\n                requestHost.isVisible = false\n                path.isVisible = true\n            }\n        }\n    }\n\n    fun updateTle(tle: String) {\n        val isTLS = tle == \"tls\"\n        val isXTLS = tle == \"xtls\"\n        securityCategory.isVisible = isTLS || isXTLS\n        xtlsFlow.isVisible = isXTLS\n        if (isXTLS) {\n            if (DataStore.serverFlow !in xtlsFlowValue) {\n                xtlsFlow.value = xtlsFlowValue[0]\n            } else {\n                xtlsFlow.value = DataStore.serverFlow\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/TrojanGoSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.app.Activity\nimport android.content.BroadcastReceiver\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.view.View\nimport androidx.activity.result.component1\nimport androidx.activity.result.component2\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.fragment.app.setFragmentResultListener\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.whenCreated\nimport androidx.preference.EditTextPreference\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceCategory\nimport com.github.shadowsocks.plugin.*\nimport com.github.shadowsocks.plugin.fragment.AlertDialogFragment\nimport com.github.shadowsocks.preference.PluginConfigurationDialogFragment\nimport com.github.shadowsocks.preference.PluginPreference\nimport com.github.shadowsocks.preference.PluginPreferenceDialogFragment\nimport com.google.android.material.dialog.MaterialAlertDialogBuilder\nimport com.google.android.material.snackbar.Snackbar\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean\nimport io.nekohasekai.sagernet.ktx.*\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass TrojanGoSettingsActivity : ProfileSettingsActivity<TrojanGoBean>(),\n    Preference.OnPreferenceChangeListener {\n\n    override fun createEntity() = TrojanGoBean()\n\n    private lateinit var plugin: PluginPreference\n    private lateinit var pluginConfigure: EditTextPreference\n    private lateinit var pluginConfiguration: PluginConfiguration\n    private lateinit var receiver: BroadcastReceiver\n\n    override fun TrojanGoBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverPassword = password\n        DataStore.serverSNI = sni\n        DataStore.serverNetwork = type\n        DataStore.serverHost = host\n        DataStore.serverPath = path\n        if (encryption.startsWith(\"ss;\")) {\n            DataStore.serverEncryption = \"ss\"\n            DataStore.serverMethod = encryption.substringAfter(\";\").substringBefore(\":\")\n            DataStore.serverPassword1 = encryption.substringAfter(\":\")\n        } else {\n            DataStore.serverEncryption = encryption\n        }\n        DataStore.serverPlugin = plugin\n    }\n\n    override fun TrojanGoBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        password = DataStore.serverPassword\n        sni = DataStore.serverSNI\n        type = DataStore.serverNetwork\n        host = DataStore.serverHost\n        path = DataStore.serverPath\n        encryption = when (val security = DataStore.serverEncryption) {\n            \"ss\" -> {\n                \"ss;\" + DataStore.serverMethod + \":\" + DataStore.serverPassword1\n            }\n            else -> {\n                security\n            }\n        }\n        plugin = DataStore.serverPlugin\n    }\n\n    override fun onAttachedToWindow() {\n        super.onAttachedToWindow()\n\n        receiver = listenForPackageChanges(false) {\n            lifecycleScope.launch(Dispatchers.Main) {   // wait until changes were flushed\n                whenCreated { initPlugins() }\n            }\n        }\n    }\n\n    lateinit var network: SimpleMenuPreference\n    lateinit var encryprtion: SimpleMenuPreference\n    lateinit var wsCategory: PreferenceCategory\n    lateinit var ssCategory: PreferenceCategory\n    lateinit var method: SimpleMenuPreference\n\n    val trojanGoMethods = app.resources.getStringArray(R.array.trojan_go_methods)\n    val trojanGoNetworks = app.resources.getStringArray(R.array.trojan_go_networks_value)\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.trojan_go_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD1)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n        wsCategory = findPreference(Key.SERVER_WS_CATEGORY)!!\n        ssCategory = findPreference(Key.SERVER_SS_CATEGORY)!!\n        method = findPreference(Key.SERVER_METHOD)!!\n\n        network = findPreference(Key.SERVER_NETWORK)!!\n\n        if (network.value !in trojanGoNetworks) {\n            network.value = trojanGoNetworks[0]\n        }\n\n        updateNetwork(network.value)\n        network.setOnPreferenceChangeListener { _, newValue ->\n            updateNetwork(newValue as String)\n            true\n        }\n        encryprtion = findPreference(Key.SERVER_ENCRYPTION)!!\n        updateEncryption(encryprtion.value)\n        encryprtion.setOnPreferenceChangeListener { _, newValue ->\n            updateEncryption(newValue as String)\n            true\n        }\n\n        plugin = findPreference(Key.SERVER_PLUGIN)!!\n        pluginConfigure = findPreference(Key.SERVER_PLUGIN_CONFIGURE)!!\n        pluginConfigure.setOnBindEditTextListener(EditTextPreferenceModifiers.Monospace)\n        pluginConfigure.onPreferenceChangeListener = this@TrojanGoSettingsActivity\n        pluginConfiguration = PluginConfiguration(DataStore.serverPlugin)\n        initPlugins()\n    }\n\n    fun updateNetwork(newNet: String) {\n        when (newNet) {\n            \"ws\" -> {\n                wsCategory.isVisible = true\n            }\n            else -> {\n                wsCategory.isVisible = false\n            }\n        }\n    }\n\n    fun updateEncryption(encryption: String) {\n        when (encryption) {\n            \"ss\" -> {\n                ssCategory.isVisible = true\n\n                if (method.value !in trojanGoMethods) {\n                    method.value = trojanGoMethods[0]\n                }\n            }\n            else -> {\n                ssCategory.isVisible = false\n            }\n        }\n    }\n\n\n    override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {\n        setFragmentResultListener(PluginPreferenceDialogFragment::class.java.name) { _, bundle ->\n            val selected = plugin.plugins.lookup.getValue(\n                bundle.getString(PluginPreferenceDialogFragment.KEY_SELECTED_ID)!!)\n            val override = pluginConfiguration.pluginsOptions.keys.firstOrNull {\n                plugin.plugins.lookup[it] == selected\n            }\n            pluginConfiguration =\n                PluginConfiguration(pluginConfiguration.pluginsOptions, override ?: selected.id)\n            DataStore.serverPlugin = pluginConfiguration.toString()\n            DataStore.dirty = true\n            plugin.value = pluginConfiguration.selected\n            pluginConfigure.isEnabled = selected !is NoPlugin\n            pluginConfigure.text = pluginConfiguration.getOptions().toString()\n            if (!selected.trusted) {\n                Snackbar.make(requireView(), R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()\n            }\n        }\n        AlertDialogFragment.setResultListener<Empty>(this,\n            UnsavedChangesDialogFragment::class.java.simpleName) { which, _ ->\n            when (which) {\n                DialogInterface.BUTTON_POSITIVE -> {\n                    runOnDefaultDispatcher {\n                        saveAndExit()\n                    }\n                }\n                DialogInterface.BUTTON_NEGATIVE -> requireActivity().finish()\n            }\n        }\n    }\n\n\n    private fun initPlugins() {\n        plugin.value = pluginConfiguration.selected\n        plugin.init(true)\n        pluginConfigure.isEnabled = plugin.selectedEntry?.let { it is NoPlugin } == false\n        pluginConfigure.text = pluginConfiguration.getOptions().toString()\n    }\n\n    private fun showPluginEditor() {\n        PluginConfigurationDialogFragment().apply {\n            setArg(Key.SERVER_PLUGIN_CONFIGURE, pluginConfiguration.selected)\n            setTargetFragment(child, 0)\n        }.showAllowingStateLoss(supportFragmentManager, Key.SERVER_PLUGIN_CONFIGURE)\n    }\n\n    override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean = try {\n        val selected = pluginConfiguration.selected\n        pluginConfiguration = PluginConfiguration((pluginConfiguration.pluginsOptions +\n                (pluginConfiguration.selected to PluginOptions(selected,\n                    newValue as? String?))).toMutableMap(),\n            selected)\n        DataStore.serverPlugin = pluginConfiguration.toString()\n        DataStore.dirty = true\n        true\n    } catch (exc: RuntimeException) {\n        Snackbar.make(child.requireView(), exc.readableMessage, Snackbar.LENGTH_LONG).show()\n        false\n    }\n\n    private val configurePlugin =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->\n            when (resultCode) {\n                Activity.RESULT_OK -> {\n                    val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)\n                    pluginConfigure.text = options\n                    onPreferenceChange(null, options)\n                }\n                PluginContract.RESULT_FALLBACK -> showPluginEditor()\n            }\n        }\n\n    override fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {\n        when (preference.key) {\n            Key.SERVER_PLUGIN -> PluginPreferenceDialogFragment().apply {\n                setArg(Key.SERVER_PLUGIN)\n                setTargetFragment(child, 0)\n            }.showAllowingStateLoss(supportFragmentManager, Key.SERVER_PLUGIN)\n            Key.SERVER_PLUGIN_CONFIGURE -> {\n                val intent = PluginManager.buildIntent(plugin.selectedEntry!!.id,\n                    PluginContract.ACTION_CONFIGURE)\n                if (intent.resolveActivity(packageManager) == null) showPluginEditor() else {\n                    configurePlugin.launch(intent\n                        .putExtra(PluginContract.EXTRA_OPTIONS,\n                            pluginConfiguration.getOptions().toString()))\n                }\n            }\n            else -> return false\n        }\n        return true\n    }\n\n    val pluginHelp =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->\n            if (resultCode == Activity.RESULT_OK) MaterialAlertDialogBuilder(this)\n                .setTitle(\"?\")\n                .setMessage(data?.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))\n                .show()\n        }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/TrojanSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport androidx.preference.SwitchPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.trojan.TrojanBean\nimport io.nekohasekai.sagernet.ktx.app\n\nclass TrojanSettingsActivity : ProfileSettingsActivity<TrojanBean>() {\n\n    override fun createEntity() = TrojanBean()\n\n    override fun TrojanBean.init() {\n        DataStore.profileName = name\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n        DataStore.serverPassword = password\n        DataStore.serverSecurity = security\n        DataStore.serverSNI = sni\n        DataStore.serverALPN = alpn\n        DataStore.serverFlow = flow\n        DataStore.serverAllowInsecure = allowInsecure\n    }\n\n    override fun TrojanBean.serialize() {\n        name = DataStore.profileName\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n        password = DataStore.serverPassword\n        security = DataStore.serverSecurity\n        sni = DataStore.serverSNI\n        alpn = DataStore.serverALPN\n        flow = DataStore.serverFlow\n        allowInsecure = DataStore.serverAllowInsecure\n    }\n\n    lateinit var security: SimpleMenuPreference\n    lateinit var tlsSni: EditTextPreference\n    lateinit var tlsAlpn: EditTextPreference\n    lateinit var xtlsFlow: SimpleMenuPreference\n    lateinit var allowInsecure: SwitchPreference\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.trojan_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n\n        security = findPreference(Key.SERVER_SECURITY)!!\n        tlsSni = findPreference(Key.SERVER_SNI)!!\n        tlsAlpn = findPreference(Key.SERVER_ALPN)!!\n        xtlsFlow = findPreference(Key.SERVER_FLOW)!!\n        allowInsecure = findPreference(Key.SERVER_ALLOW_INSECURE)!!\n\n        updateTle(security.value)\n        security.setOnPreferenceChangeListener { _, newValue ->\n            updateTle(newValue as String)\n            true\n        }\n    }\n\n    val xtlsFlowValue = app.resources.getStringArray(R.array.xtls_flow_value)\n\n    fun updateTle(tle: String) {\n        when (tle) {\n            \"tls\" -> {\n                xtlsFlow.isVisible = false\n            }\n            \"xtls\" -> {\n                xtlsFlow.isVisible = true\n\n                if (DataStore.serverFlow !in xtlsFlowValue) {\n                    xtlsFlow.value = xtlsFlowValue[0]\n                } else {\n                    xtlsFlow.value = DataStore.serverFlow\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/VLESSSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport io.nekohasekai.sagernet.fmt.v2ray.VLESSBean\n\nclass VLESSSettingsActivity : StandardV2RaySettingsActivity() {\n\n    override fun createEntity() = VLESSBean()\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/VMessSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport io.nekohasekai.sagernet.fmt.v2ray.VMessBean\n\nclass VMessSettingsActivity : StandardV2RaySettingsActivity() {\n\n    override fun createEntity() = VMessBean()\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/ui/profile/WireGuardSettingsActivity.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ui.profile\n\nimport android.os.Bundle\nimport androidx.preference.EditTextPreference\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.Key\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers\nimport io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean\n\nclass WireGuardSettingsActivity : ProfileSettingsActivity<WireGuardBean>() {\n\n    override fun createEntity() = WireGuardBean()\n\n    override fun WireGuardBean.init() {\n        DataStore.profileName = name\n\n        DataStore.serverLocalAddress = localAddress\n        DataStore.serverPrivateKey = privateKey\n\n        DataStore.serverAddress = serverAddress\n        DataStore.serverPort = serverPort\n\n        DataStore.serverCertificates = peerPublicKey\n        DataStore.serverPassword = peerPreSharedKey\n    }\n\n    override fun WireGuardBean.serialize() {\n        name = DataStore.profileName\n\n        localAddress = DataStore.serverLocalAddress\n        privateKey = DataStore.serverPrivateKey\n\n        serverAddress = DataStore.serverAddress\n        serverPort = DataStore.serverPort\n\n        peerPublicKey = DataStore.serverCertificates\n        peerPreSharedKey = DataStore.serverPassword\n    }\n\n    override fun PreferenceFragmentCompat.createPreferences(\n        savedInstanceState: Bundle?,\n        rootKey: String?,\n    ) {\n        addPreferencesFromResource(R.xml.wireguard_preferences)\n        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {\n            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)\n        }\n        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {\n            summaryProvider = PasswordSummaryProvider\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport com.wireguard.crypto.KeyPair\nimport io.nekohasekai.sagernet.fmt.gson.gson\nimport io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean\nimport io.nekohasekai.sagernet.ktx.createProxyClient\nimport io.nekohasekai.sagernet.utils.cf.DeviceResponse\nimport io.nekohasekai.sagernet.utils.cf.RegisterRequest\nimport io.nekohasekai.sagernet.utils.cf.UpdateDeviceRequest\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport okhttp3.internal.closeQuietly\n\n// kang from wgcf\nobject Cloudflare {\n\n    private const val API_URL = \"https://api.cloudflareclient.com\"\n    private const val API_VERSION = \"v0a1922\"\n\n    private const val CLIENT_VERSION_KEY = \"CF-Client-Version\"\n    private const val CLIENT_VERSION = \"a-6.3-1922\"\n\n    fun makeWireGuardConfiguration(): WireGuardBean {\n        val keyPair = KeyPair()\n        val okhttpClient = createProxyClient()\n        var body = RegisterRequest.newRequest(keyPair.publicKey)\n        var response = okhttpClient.newCall(\n            Request.Builder()\n                .url(\"$API_URL/$API_VERSION/reg\")\n                .header(\"Accept\", \"application/json\")\n                .header(CLIENT_VERSION_KEY, CLIENT_VERSION)\n                .post(body.toRequestBody(\"application/json\".toMediaType()))\n                .build()\n        ).execute()\n        if (!response.isSuccessful) error(response)\n        val device = gson.fromJson(response.body!!.string(), DeviceResponse::class.java)\n        val accessToken = device.token\n        body = UpdateDeviceRequest.newRequest()\n        response = okhttpClient.newCall(\n            Request.Builder()\n                .url(API_URL + \"/\" + API_VERSION + \"/reg/\" + device.id + \"/account/reg/\" + device.id)\n                .header(\"Authorization\", \"Bearer $accessToken\")\n                .header(\"Accept\", \"application/json\")\n                .header(CLIENT_VERSION_KEY, CLIENT_VERSION)\n                .patch(body.toRequestBody(\"application/json\".toMediaType()))\n                .build()\n        ).execute()\n        try {\n            if (!response.isSuccessful) error(response)\n            val peer = device.config.peers[0]\n            val localAddresses = device.config.interfaceX.addresses\n            return WireGuardBean().apply {\n                name = \"CloudFlare Warp ${device.account.id}\"\n                privateKey = keyPair.privateKey.toBase64()\n                peerPublicKey = peer.publicKey\n                serverAddress = peer.endpoint.host.substringBeforeLast(\":\")\n                serverPort = peer.endpoint.host.substringAfterLast(\":\").toInt()\n                localAddress = localAddresses.v4 + \"\\n\" + localAddresses.v6\n            }\n        } finally {\n            response.body?.closeQuietly()\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport java.util.*\n\n/**\n * Commandline objects help handling command lines specifying processes to\n * execute.\n *\n * The class can be used to define a command line as nested elements or as a\n * helper to define a command line by an application.\n *\n *\n * `\n * <someelement><br></br>\n * &nbsp;&nbsp;<acommandline executable=\"/executable/to/run\"><br></br>\n * &nbsp;&nbsp;&nbsp;&nbsp;<argument value=\"argument 1\" /><br></br>\n * &nbsp;&nbsp;&nbsp;&nbsp;<argument line=\"argument_1 argument_2 argument_3\" /><br></br>\n * &nbsp;&nbsp;&nbsp;&nbsp;<argument value=\"argument 4\" /><br></br>\n * &nbsp;&nbsp;</acommandline><br></br>\n * </someelement><br></br>\n` *\n *\n * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java\n *\n * Adds support for escape character '\\'.\n */\nobject Commandline {\n\n    /**\n     * Quote the parts of the given array in way that makes them\n     * usable as command line arguments.\n     * @param args the list of arguments to quote.\n     * @return empty string for null or no command, else every argument split\n     * by spaces and quoted by quoting rules.\n     */\n    fun toString(args: Iterable<String>?): String {\n        // empty path return empty string\n        args ?: return \"\"\n        // path containing one or more elements\n        val result = StringBuilder()\n        for (arg in args) {\n            if (result.isNotEmpty()) result.append(' ')\n            arg.indices.map { arg[it] }.forEach {\n                when (it) {\n                    ' ', '\\\\', '\"', '\\'' -> {\n                        result.append('\\\\')  // intentionally no break\n                        result.append(it)\n                    }\n                    else -> result.append(it)\n                }\n            }\n        }\n        return result.toString()\n    }\n\n    /**\n     * Quote the parts of the given array in way that makes them\n     * usable as command line arguments.\n     * @param args the list of arguments to quote.\n     * @return empty string for null or no command, else every argument split\n     * by spaces and quoted by quoting rules.\n     */\n    fun toString(args: Array<String>) =\n        toString(args.asIterable()) // thanks to Java, arrays aren't iterable\n\n    /**\n     * Crack a command line.\n     * @param toProcess the command line to process.\n     * @return the command line broken into strings.\n     * An empty or null toProcess parameter results in a zero sized array.\n     */\n    fun translateCommandline(toProcess: String?): Array<String> {\n        if (toProcess == null || toProcess.isEmpty()) {\n            //no command? no string\n            return arrayOf()\n        }\n        // parse with a simple finite state machine\n\n        val normal = 0\n        val inQuote = 1\n        val inDoubleQuote = 2\n        var state = normal\n        val tok = StringTokenizer(toProcess, \"\\\\\\\"\\' \", true)\n        val result = ArrayList<String>()\n        val current = StringBuilder()\n        var lastTokenHasBeenQuoted = false\n        var lastTokenIsSlash = false\n\n        while (tok.hasMoreTokens()) {\n            val nextTok = tok.nextToken()\n            when (state) {\n                inQuote -> if (\"\\'\" == nextTok) {\n                    lastTokenHasBeenQuoted = true\n                    state = normal\n                } else current.append(nextTok)\n                inDoubleQuote -> when (nextTok) {\n                    \"\\\"\" -> if (lastTokenIsSlash) {\n                        current.append(nextTok)\n                        lastTokenIsSlash = false\n                    } else {\n                        lastTokenHasBeenQuoted = true\n                        state = normal\n                    }\n                    \"\\\\\" -> lastTokenIsSlash = if (lastTokenIsSlash) {\n                        current.append(nextTok)\n                        false\n                    } else true\n                    else -> {\n                        if (lastTokenIsSlash) {\n                            current.append(\"\\\\\")   // unescaped\n                            lastTokenIsSlash = false\n                        }\n                        current.append(nextTok)\n                    }\n                }\n                else -> {\n                    when {\n                        lastTokenIsSlash -> {\n                            current.append(nextTok)\n                            lastTokenIsSlash = false\n                        }\n                        \"\\\\\" == nextTok -> lastTokenIsSlash = true\n                        \"\\'\" == nextTok -> state = inQuote\n                        \"\\\"\" == nextTok -> state = inDoubleQuote\n                        \" \" == nextTok -> if (lastTokenHasBeenQuoted || current.isNotEmpty()) {\n                            result.add(current.toString())\n                            current.setLength(0)\n                        }\n                        else -> current.append(nextTok)\n                    }\n                    lastTokenHasBeenQuoted = false\n                }\n            }\n        }\n        if (lastTokenHasBeenQuoted || current.isNotEmpty()) result.add(current.toString())\n        require(state != inQuote && state != inDoubleQuote) { \"unbalanced quotes in $toProcess\" }\n        require(!lastTokenIsSlash) { \"escape character following nothing in $toProcess\" }\n        return result.toTypedArray()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.os.Build\nimport androidx.core.content.FileProvider\nimport com.jakewharton.processphoenix.ProcessPhoenix\nimport io.nekohasekai.sagernet.BuildConfig\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.preference.PublicDatabase\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.use\nimport java.io.*\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport java.util.regex.Pattern\n\nobject CrashHandler : Thread.UncaughtExceptionHandler {\n\n    @Suppress(\"UNNECESSARY_SAFE_CALL\")\n    override fun uncaughtException(thread: Thread, throwable: Throwable) {\n\n        val logFile = File.createTempFile(\"AnXray Crash Report \",\n            \".log\",\n            File(app.cacheDir, \"log\").also { it.mkdirs() }\n        )\n\n        var report = buildReportHeader()\n\n        report += \"\\n\"\n        report += \"Thread: $thread\\n\\n\"\n\n        report += formatThrowable(throwable) + \"\\n\\n\"\n\n        report += \"Logcat: \\n\\n\"\n\n        logFile.writeText(report)\n\n        try {\n            Runtime.getRuntime().exec(arrayOf(\"logcat\", \"-d\")).inputStream.use(\n                FileOutputStream(\n                    logFile, true\n                )\n            )\n        } catch (e: IOException) {\n            Logs.w(e)\n            logFile.appendText(\"Export logcat error: \" + formatThrowable(e))\n        }\n\n        ProcessPhoenix.triggerRebirth(\n            app, Intent.createChooser(\n                Intent(Intent.ACTION_SEND).setType(\"text/x-log\")\n                    .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                    .putExtra(\n                        Intent.EXTRA_STREAM, FileProvider.getUriForFile(\n                            app, BuildConfig.APPLICATION_ID + \".log\", logFile\n                        )\n                    ), app.getString(R.string.abc_shareactionprovider_share_with)\n            )\n        )\n\n    }\n\n    fun formatThrowable(throwable: Throwable): String {\n        var format = throwable.javaClass.name\n        val message = throwable.message\n        if (!message.isNullOrBlank()) {\n            format += \": $message\"\n        }\n        format += \"\\n\"\n\n        format += throwable.stackTrace.joinToString(\"\\n\") {\n            \"    at ${it.className}.${it.methodName}(${it.fileName}:${if (it.isNativeMethod) \"native\" else it.lineNumber})\"\n        }\n\n        val cause = throwable.cause\n        if (cause != null) {\n            format += \"\\n\\nCaused by: \" + formatThrowable(cause)\n        }\n\n        return format\n    }\n\n    fun buildReportHeader(): String {\n        var report = \"\"\n        report += \"SagerNet ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) ${BuildConfig.FLAVOR.uppercase()}\\n\"\n        report += \"Date: ${getCurrentMilliSecondUTCTimeStamp()}\\n\\n\"\n        report += \"OS_VERSION: ${getSystemPropertyWithAndroidAPI(\"os.version\")}\\n\"\n        report += \"SDK_INT: ${Build.VERSION.SDK_INT}\\n\"\n        report += if (\"REL\" == Build.VERSION.CODENAME) {\n            \"RELEASE: ${Build.VERSION.RELEASE}\"\n        } else {\n            \"CODENAME: ${Build.VERSION.CODENAME}\"\n        } + \"\\n\"\n        report += \"ID: ${Build.ID}\\n\"\n        report += \"DISPLAY: ${Build.DISPLAY}\\n\"\n        report += \"INCREMENTAL: ${Build.VERSION.INCREMENTAL}\\n\"\n\n        val systemProperties = getSystemProperties()\n\n        report += \"SECURITY_PATCH: ${systemProperties.getProperty(\"ro.build.version.security_patch\")}\\n\"\n        report += \"IS_DEBUGGABLE: ${systemProperties.getProperty(\"ro.debuggable\")}\\n\"\n        report += \"IS_EMULATOR: ${systemProperties.getProperty(\"ro.boot.qemu\")}\\n\"\n        report += \"IS_TREBLE_ENABLED: ${systemProperties.getProperty(\"ro.treble.enabled\")}\\n\"\n\n        report += \"TYPE: ${Build.TYPE}\\n\"\n        report += \"TAGS: ${Build.TAGS}\\n\\n\"\n\n        report += \"MANUFACTURER: ${Build.MANUFACTURER}\\n\"\n        report += \"BRAND: ${Build.BRAND}\\n\"\n        report += \"MODEL: ${Build.MODEL}\\n\"\n        report += \"PRODUCT: ${Build.PRODUCT}\\n\"\n        report += \"BOARD: ${Build.BOARD}\\n\"\n        report += \"HARDWARE: ${Build.HARDWARE}\\n\"\n        report += \"DEVICE: ${Build.DEVICE}\\n\"\n        report += \"SUPPORTED_ABIS: ${\n            Build.SUPPORTED_ABIS.filter { it.isNotBlank() }.joinToString(\", \")\n        }\\n\\n\"\n\n\n        try {\n            report += \"Settings: \\n\"\n            for (pair in PublicDatabase.kvPairDao.all()) {\n                report += \"\\n\"\n                report += pair.key + \": \" + pair.toString()\n            }\n        }catch (e: Exception) {\n            report += \"Export settings failed: \" + formatThrowable(e)\n        }\n\n        report += \"\\n\\n\"\n\n        return report\n    }\n\n    private fun getSystemProperties(): Properties {\n        val systemProperties = Properties()\n\n        // getprop commands returns values in the format `[key]: [value]`\n        // Regex matches string starting with a literal `[`,\n        // followed by one or more characters that do not match a closing square bracket as the key,\n        // followed by a literal `]: [`,\n        // followed by one or more characters as the value,\n        // followed by string ending with literal `]`\n        // multiline values will be ignored\n        val propertiesPattern = Pattern.compile(\"^\\\\[([^]]+)]: \\\\[(.+)]$\")\n        try {\n            val process = ProcessBuilder().command(\"/system/bin/getprop\")\n                .redirectErrorStream(true)\n                .start()\n            val inputStream = process.inputStream\n            val bufferedReader = BufferedReader(InputStreamReader(inputStream))\n            var line: String?\n            var key: String\n            var value: String\n            while (bufferedReader.readLine().also { line = it } != null) {\n                val matcher = propertiesPattern.matcher(line)\n                if (matcher.matches()) {\n                    key = matcher.group(1)\n                    value = matcher.group(2)\n                    if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] = value\n                }\n            }\n            bufferedReader.close()\n            process.destroy()\n        } catch (e: IOException) {\n            Logs.e(\n                \"Failed to get run \\\"/system/bin/getprop\\\" to get system properties.\", e\n            )\n        }\n\n        //for (String key : systemProperties.stringPropertyNames()) {\n        //    Logger.logVerbose(key + \": \" +  systemProperties.get(key));\n        //}\n        return systemProperties\n    }\n\n    private fun getSystemPropertyWithAndroidAPI(property: String): String? {\n        return try {\n            System.getProperty(property)\n        } catch (e: Exception) {\n            Logs.e(\"Failed to get system property \\\"\" + property + \"\\\":\" + e.message)\n            null\n        }\n    }\n\n    @SuppressLint(\"SimpleDateFormat\")\n    private fun getCurrentMilliSecondUTCTimeStamp(): String {\n        val df = SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS z\")\n        df.timeZone = TimeZone.getTimeZone(\"UTC\")\n        return df.format(Date())\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/DefaultNetworkListener.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.annotation.TargetApi\nimport android.net.ConnectivityManager\nimport android.net.Network\nimport android.net.NetworkCapabilities\nimport android.net.NetworkRequest\nimport android.os.Build\nimport android.os.Handler\nimport android.os.Looper\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.ktx.Logs\nimport kotlinx.coroutines.CompletableDeferred\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.channels.actor\nimport kotlinx.coroutines.runBlocking\nimport java.net.UnknownHostException\n\nobject DefaultNetworkListener {\n    private sealed class NetworkMessage {\n        class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()\n        class Get : NetworkMessage() {\n            val response = CompletableDeferred<Network>()\n        }\n\n        class Stop(val key: Any) : NetworkMessage()\n\n        class Put(val network: Network) : NetworkMessage()\n        class Update(val network: Network) : NetworkMessage()\n        class Lost(val network: Network) : NetworkMessage()\n    }\n\n    private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {\n        val listeners = mutableMapOf<Any, (Network?) -> Unit>()\n        var network: Network? = null\n        val pendingRequests = arrayListOf<NetworkMessage.Get>()\n        for (message in channel) when (message) {\n            is NetworkMessage.Start -> {\n                if (listeners.isEmpty()) register()\n                listeners[message.key] = message.listener\n                if (network != null) message.listener(network)\n            }\n            is NetworkMessage.Get -> {\n                check(listeners.isNotEmpty()) { \"Getting network without any listeners is not supported\" }\n                if (network == null) pendingRequests += message else message.response.complete(\n                    network\n                )\n            }\n            is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty\n                listeners.remove(message.key) != null && listeners.isEmpty()\n            ) {\n                network = null\n                unregister()\n            }\n\n            is NetworkMessage.Put -> {\n                network = message.network\n                pendingRequests.forEach { it.response.complete(message.network) }\n                pendingRequests.clear()\n                listeners.values.forEach { it(network) }\n            }\n            is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach {\n                it(\n                    network\n                )\n            }\n            is NetworkMessage.Lost -> if (network == message.network) {\n                network = null\n                listeners.values.forEach { it(null) }\n            }\n        }\n    }\n\n    suspend fun start(key: Any, listener: (Network?) -> Unit) =\n        networkActor.send(NetworkMessage.Start(key, listener))\n\n    suspend fun get() = if (fallback) @TargetApi(23) {\n        SagerNet.connectivity.activeNetwork\n            ?: throw UnknownHostException() // failed to listen, return current if available\n    } else NetworkMessage.Get().run {\n        networkActor.send(this)\n        response.await()\n    }\n\n    suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))\n\n    // NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26\n    private object Callback : ConnectivityManager.NetworkCallback() {\n        override fun onAvailable(network: Network) =\n            runBlocking { networkActor.send(NetworkMessage.Put(network)) }\n\n        override fun onCapabilitiesChanged(\n            network: Network, networkCapabilities: NetworkCapabilities\n        ) { // it's a good idea to refresh capabilities\n            runBlocking { networkActor.send(NetworkMessage.Update(network)) }\n        }\n\n        override fun onLost(network: Network) =\n            runBlocking { networkActor.send(NetworkMessage.Lost(network)) }\n    }\n\n    private var fallback = false\n    private val request = NetworkRequest.Builder().apply {\n        addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)\n        addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)\n        if (Build.VERSION.SDK_INT == 23) {  // workarounds for OEM bugs\n            removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)\n            removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)\n        }\n    }.build()\n    private val mainHandler = Handler(Looper.getMainLooper())\n\n    /**\n     * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:\n     * https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e\n     *\n     * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that\n     * satisfies default network capabilities but only THE default network. Unfortunately, we need to have\n     * android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.\n     *\n     * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887\n     */\n    private fun register() {\n        try {\n            fallback = false\n            when (Build.VERSION.SDK_INT) {\n                in 31..Int.MAX_VALUE -> @TargetApi(31) {\n                    SagerNet.connectivity.registerBestMatchingNetworkCallback(\n                        request, Callback, mainHandler\n                    )\n                }\n                in 28 until 31 -> @TargetApi(28) {  // we want REQUEST here instead of LISTEN\n                    SagerNet.connectivity.requestNetwork(request, Callback, mainHandler)\n                }\n                in 26 until 28 -> @TargetApi(26) {\n                    SagerNet.connectivity.registerDefaultNetworkCallback(Callback, mainHandler)\n                }\n                in 24 until 26 -> @TargetApi(24) {\n                    SagerNet.connectivity.registerDefaultNetworkCallback(Callback)\n                }\n                else -> {\n                    SagerNet.connectivity.requestNetwork(request, Callback)\n                    // known bug on API 23: https://stackoverflow.com/a/33509180/2245107\n                }\n            }\n        } catch (e: Exception) {\n            Logs.w(e)\n            fallback = true\n        }\n    }\n\n    private fun unregister() = SagerNet.connectivity.unregisterNetworkCallback(Callback)\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/DeviceStorageApp.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.annotation.SuppressLint\nimport android.annotation.TargetApi\nimport android.app.Application\nimport android.content.Context\n\n@SuppressLint(\"Registered\")\n@TargetApi(24)\nclass DeviceStorageApp(context: Context) : Application() {\n    init {\n        attachBaseContext(context.createDeviceProtectedStorageContext())\n    }\n\n    /**\n     * Thou shalt not get the REAL underlying application context which would no longer be operating under device\n     * protected storage.\n     */\n    override fun getApplicationContext() = this\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/DirectBoot.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.annotation.TargetApi\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport io.nekohasekai.sagernet.SagerNet\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\nimport io.nekohasekai.sagernet.database.ProxyEntity\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.marshall\nimport io.nekohasekai.sagernet.ktx.unmarshall\nimport kotlinx.coroutines.runBlocking\nimport java.io.File\nimport java.io.IOException\n\n@TargetApi(24)\nobject DirectBoot : BroadcastReceiver() {\n    private val file = File(SagerNet.deviceStorage.noBackupFilesDir, \"directBootProfile\")\n    private var registered = false\n\n    fun getDeviceProfile(): ProxyEntity? = try {\n        file.readBytes().unmarshall(::ProxyEntity)\n    } catch (_: IOException) {\n        null\n    }\n\n    fun clean() {\n        file.delete()\n        // File(SagerNet.deviceStorage.noBackupFilesDir, BaseService.CONFIG_FILE).delete()\n    }\n\n    /**\n     * app.currentProfile will call this.\n     */\n    fun update(profile: ProxyEntity? = ProfileManager.getProfile(DataStore.selectedProxy)) =\n        if (profile == null) clean()\n        else file.writeBytes(profile.marshall())\n\n    fun flushTrafficStats() {\n        getDeviceProfile()?.also {\n            runBlocking {\n                if (it.dirty) ProfileManager.updateProfile(it)\n            }\n        }\n        update()\n    }\n\n    fun listenForUnlock() {\n        if (registered) return\n        app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))\n        registered = true\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        flushTrafficStats()\n        app.unregisterReceiver(this)\n        registered = false\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/HttpsTest.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.os.SystemClock\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewModel\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.*\nimport kotlinx.coroutines.delay\nimport okhttp3.Call\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.internal.closeQuietly\nimport java.io.IOException\n\n/**\n * Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071\n */\nclass HttpsTest : ViewModel() {\n    sealed class Status {\n        protected abstract val status: CharSequence\n        open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) =\n            setStatus(status)\n\n        object Idle : Status() {\n            override val status get() = app.getText(R.string.vpn_connected)\n        }\n\n        object Testing : Status() {\n            override val status get() = app.getText(R.string.connection_test_testing)\n        }\n\n        class Success(private val elapsed: Long) : Status() {\n            override val status\n                get() = app.getString(\n                    if (DataStore.connectionTestURL.startsWith(\"https://\")) {\n                        R.string.connection_test_available\n                    } else {\n                        R.string.connection_test_available_http\n                    }, elapsed\n                )\n        }\n\n        sealed class Error : Status() {\n            override val status get() = app.getText(R.string.connection_test_fail)\n            protected abstract val error: String\n            private var shown = false\n            override fun retrieve(\n                setStatus: (CharSequence) -> Unit,\n                errorCallback: (String) -> Unit,\n            ) {\n                super.retrieve(setStatus, errorCallback)\n                if (shown) return\n                shown = true\n                errorCallback(error)\n            }\n\n            class UnexpectedResponseCode(private val code: Int) : Error() {\n                override val error\n                    get() = app.getString(\n                        R.string.connection_test_error_status_code, code\n                    )\n            }\n\n            class IOFailure(private val e: IOException) : Error() {\n                override val error get() = app.getString(R.string.connection_test_error, e.message)\n            }\n        }\n\n    }\n\n    private var running: Call? = null\n    val status = MutableLiveData<Status>(Status.Idle)\n    val okHttpClient by lazy { OkHttpClient.Builder().proxy(requireProxy()).build() }\n\n    fun testConnection() {\n        cancelTest()\n        status.value = Status.Testing\n\n        runOnDefaultDispatcher {\n            val start = SystemClock.elapsedRealtime()\n            running = okHttpClient.newCall(\n                Request.Builder()\n                    .url(DataStore.connectionTestURL)\n                    .addHeader(\"Connection\", \"close\")\n                    .addHeader(\"User-Agent\", USER_AGENT)\n                    .build()\n            ).apply {\n                val response = try {\n                    execute()\n                } catch (e: IOException) {\n                    if (e.readableMessage.contains(\"failed to connect to /127.0.0.1\") && e.readableMessage.contains(\n                            \"ECONNREFUSED\"\n                        )\n                    ) {\n                        delay(1000L)\n                        onMainDispatcher {\n                            testConnection()\n                        }\n                        return@runOnDefaultDispatcher\n                    }\n                    if (!isCanceled()) {\n                        onMainDispatcher {\n                            status.value = Status.Error.IOFailure(e)\n                            running = null\n                        }\n                    }\n                    return@runOnDefaultDispatcher\n                }\n\n                if (isCanceled()) {\n                    return@runOnDefaultDispatcher\n                }\n\n                val code = response.code\n                val elapsed = SystemClock.elapsedRealtime() - start\n                response.closeQuietly()\n                runOnMainDispatcher {\n                    status.value = if (code == 204 || code == 200) {\n                        Status.Success(elapsed)\n                    } else {\n                        Status.Error.UnexpectedResponseCode(code)\n                    }\n                    running = null\n                }\n            }\n        }\n\n    }\n\n    private fun cancelTest() {\n        running?.cancel()\n        running = null\n    }\n\n    fun invalidate() {\n        cancelTest()\n        status.value = Status.Idle\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageManager\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.listenForPackageChanges\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\nobject PackageCache {\n\n    lateinit var installedPackages: Map<String, PackageInfo>\n    lateinit var installedApps: Map<String, ApplicationInfo>\n    lateinit var packageMap: Map<String, Int>\n    val uidMap = HashMap<Int, HashSet<String>>()\n    val loaded = Mutex(true)\n\n    fun register() {\n        reload()\n        app.listenForPackageChanges(false) {\n            reload()\n            labelMap.clear()\n        }\n        loaded.unlock()\n    }\n\n    @SuppressLint(\"InlinedApi\")\n    fun reload() {\n        installedPackages = app.packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES)\n            .filter {\n                when (it.packageName) {\n                    \"android\" -> true\n                    else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true\n                }\n            }\n            .associateBy { it.packageName }\n\n        val installed = app.packageManager.getInstalledApplications(PackageManager.GET_META_DATA)\n        installedApps = installed.associateBy { it.packageName }\n        packageMap = installed.associate { it.packageName to it.uid }\n        uidMap.clear()\n        for (info in installed) {\n            val uid = info.uid\n            uidMap.getOrPut(uid) { HashSet() }.add(info.packageName)\n        }\n    }\n\n    operator fun get(uid: Int) = uidMap[uid]\n    operator fun get(packageName: String) = packageMap[packageName]\n\n    suspend fun awaitLoad() {\n        if (::packageMap.isInitialized) {\n            return\n        }\n        loaded.withLock {\n            // just await\n        }\n    }\n\n    fun awaitLoadSync() {\n        if (::packageMap.isInitialized) {\n            return\n        }\n        runBlocking {\n            loaded.withLock {\n                // just await\n            }\n        }\n    }\n\n    private val labelMap = mutableMapOf<String, String>()\n    fun loadLabel(packageName: String): String {\n        var label = labelMap[packageName]\n        if (label != null) return label\n        val info = installedApps[packageName] ?: return packageName\n        label = info.loadLabel(app.packageManager).toString()\n        labelMap[packageName] = label\n        return label\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/Subnet.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport io.nekohasekai.sagernet.ktx.parseNumericAddress\nimport java.net.InetAddress\nimport java.util.*\n\nclass Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet> {\n    companion object {\n        fun fromString(value: String, lengthCheck: Int = -1): Subnet? {\n            val parts = value.split('/', limit = 2)\n            val addr = parts[0].parseNumericAddress() ?: return null\n            check(lengthCheck < 0 || addr.address.size == lengthCheck)\n            return if (parts.size == 2) try {\n                val prefixSize = parts[1].toInt()\n                if (prefixSize < 0 || prefixSize > addr.address.size shl 3) null else Subnet(addr,\n                    prefixSize)\n            } catch (_: NumberFormatException) {\n                null\n            } else Subnet(addr, addr.address.size shl 3)\n        }\n    }\n\n    private val addressLength get() = address.address.size shl 3\n\n    init {\n        require(prefixSize in 0..addressLength) { \"prefixSize $prefixSize not in 0..$addressLength\" }\n    }\n\n    class Immutable(private val a: ByteArray, private val prefixSize: Int = 0) {\n        companion object : Comparator<Immutable> {\n            override fun compare(a: Immutable, b: Immutable): Int {\n                check(a.a.size == b.a.size)\n                for (i in a.a.indices) {\n                    val result = a.a[i].compareTo(b.a[i])\n                    if (result != 0) return result\n                }\n                return 0\n            }\n        }\n\n        fun matches(b: Immutable) = matches(b.a)\n        fun matches(b: ByteArray): Boolean {\n            if (a.size != b.size) return false\n            var i = 0\n            while (i * 8 < prefixSize && i * 8 + 8 <= prefixSize) {\n                if (a[i] != b[i]) return false\n                ++i\n            }\n            return i * 8 == prefixSize || a[i] == (b[i].toInt() and -(1 shl i * 8 + 8 - prefixSize)).toByte()\n        }\n    }\n\n    fun toImmutable() = Immutable(address.address.also {\n        var i = prefixSize / 8\n        if (prefixSize % 8 > 0) {\n            it[i] = (it[i].toInt() and -(1 shl i * 8 + 8 - prefixSize)).toByte()\n            ++i\n        }\n        while (i < it.size) it[i++] = 0\n    }, prefixSize)\n\n    override fun toString(): String =\n        if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize\n\n    private fun Byte.unsigned() = toInt() and 0xFF\n    override fun compareTo(other: Subnet): Int {\n        val addrThis = address.address\n        val addrThat = other.address.address\n        var result =\n            addrThis.size.compareTo(addrThat.size)                 // IPv4 address goes first\n        if (result != 0) return result\n        for (i in addrThis.indices) {\n            result = addrThis[i].unsigned()\n                .compareTo(addrThat[i].unsigned())   // undo sign extension of signed byte\n            if (result != 0) return result\n        }\n        return prefixSize.compareTo(other.prefixSize)\n    }\n\n    override fun equals(other: Any?): Boolean {\n        val that = other as? Subnet\n        return address == that?.address && prefixSize == that.prefixSize\n    }\n\n    override fun hashCode(): Int = Objects.hash(address, prefixSize)\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils\n\nimport android.content.Context\nimport android.content.res.Configuration\nimport androidx.appcompat.app.AppCompatDelegate\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.isExpert\n\nobject Theme {\n\n    const val RED = 1\n    const val PINK = 2\n    const val PURPLE = 3\n    const val DEEP_PURPLE = 4\n    const val INDIGO = 5\n    const val BLUE = 6\n    const val LIGHT_BLUE = 7\n    const val CYAN = 8\n    const val TEAL = 9\n    const val GREEN = 10\n    const val LIGHT_GREEN = 11\n    const val LIME = 12\n    const val YELLOW = 13\n    const val AMBER = 14\n    const val ORANGE = 15\n    const val DEEP_ORANGE = 16\n\n    const val BROWN = 17\n    const val GREY = 18\n    const val BLUE_GREY = 19\n    const val BLACK = 20\n\n    private fun defaultTheme() = BLACK\n\n    fun apply(context: Context) {\n        context.setTheme(getTheme())\n    }\n\n    fun getTheme(): Int {\n        return getTheme(if (isExpert) DataStore.appTheme else defaultTheme())\n    }\n\n    fun getTheme(theme: Int): Int {\n        return when (theme) {\n            RED -> R.style.Theme_SagerNet_Red\n            PINK -> R.style.Theme_SagerNet\n            PURPLE -> R.style.Theme_SagerNet_Purple\n            DEEP_PURPLE -> R.style.Theme_SagerNet_DeepPurple\n            INDIGO -> R.style.Theme_SagerNet_Indigo\n            BLUE -> R.style.Theme_SagerNet_Blue\n            LIGHT_BLUE -> R.style.Theme_SagerNet_LightBlue\n            CYAN -> R.style.Theme_SagerNet_Cyan\n            TEAL -> R.style.Theme_SagerNet_Teal\n            GREEN -> R.style.Theme_SagerNet_Green\n            LIGHT_GREEN -> R.style.Theme_SagerNet_LightGreen\n            LIME -> R.style.Theme_SagerNet_Lime\n            YELLOW -> R.style.Theme_SagerNet_Yellow\n            AMBER -> R.style.Theme_SagerNet_Amber\n            ORANGE -> R.style.Theme_SagerNet_Orange\n            DEEP_ORANGE -> R.style.Theme_SagerNet_DeepOrange\n            BROWN -> R.style.Theme_SagerNet_Brown\n            GREY -> R.style.Theme_SagerNet_Grey\n            BLUE_GREY -> R.style.Theme_SagerNet_BlueGrey\n            BLACK -> if (usingNightMode()) R.style.Theme_SagerNet_Black else R.style.Theme_SagerNet_LightBlack\n            else -> getTheme(defaultTheme())\n        }\n    }\n\n    var currentNightMode = -1\n    fun getNightMode(): Int {\n        if (currentNightMode == -1) {\n            currentNightMode = DataStore.nightTheme\n        }\n        return getNightMode(currentNightMode)\n    }\n\n    fun getNightMode(mode: Int): Int {\n        return when (mode) {\n            0 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM\n            1 -> AppCompatDelegate.MODE_NIGHT_YES\n            2 -> AppCompatDelegate.MODE_NIGHT_NO\n            else -> AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY\n        }\n    }\n\n    fun usingNightMode(): Boolean {\n        return when (DataStore.nightTheme) {\n            1 -> true\n            2 -> false\n            else -> (app.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES\n        }\n    }\n\n    fun applyNightTheme() {\n        AppCompatDelegate.setDefaultNightMode(getNightMode())\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils.cf\n\n\nimport com.google.gson.annotations.SerializedName\n\ndata class DeviceResponse(\n    @SerializedName(\"created\")\n    var created: String = \"\",\n    @SerializedName(\"type\")\n    var type: String = \"\",\n    @SerializedName(\"locale\")\n    var locale: String = \"\",\n    @SerializedName(\"enabled\")\n    var enabled: Boolean = false,\n    @SerializedName(\"token\")\n    var token: String = \"\",\n    @SerializedName(\"waitlist_enabled\")\n    var waitlistEnabled: Boolean = false,\n    @SerializedName(\"install_id\")\n    var installId: String = \"\",\n    @SerializedName(\"warp_enabled\")\n    var warpEnabled: Boolean = false,\n    @SerializedName(\"name\")\n    var name: String = \"\",\n    @SerializedName(\"fcm_token\")\n    var fcmToken: String = \"\",\n    @SerializedName(\"tos\")\n    var tos: String = \"\",\n    @SerializedName(\"model\")\n    var model: String = \"\",\n    @SerializedName(\"id\")\n    var id: String = \"\",\n    @SerializedName(\"place\")\n    var place: Int = 0,\n    @SerializedName(\"config\")\n    var config: Config = Config(),\n    @SerializedName(\"updated\")\n    var updated: String = \"\",\n    @SerializedName(\"key\")\n    var key: String = \"\",\n    @SerializedName(\"account\")\n    var account: Account = Account()\n) {\n    data class Config(\n        @SerializedName(\"peers\")\n        var peers: List<Peer> = listOf(),\n        @SerializedName(\"services\")\n        var services: Services = Services(),\n        @SerializedName(\"interface\")\n        var interfaceX: Interface = Interface(),\n        @SerializedName(\"client_id\")\n        var clientId: String = \"\"\n    ) {\n        data class Peer(\n            @SerializedName(\"public_key\")\n            var publicKey: String = \"\",\n            @SerializedName(\"endpoint\")\n            var endpoint: Endpoint = Endpoint()\n        ) {\n            data class Endpoint(\n                @SerializedName(\"v6\")\n                var v6: String = \"\",\n                @SerializedName(\"host\")\n                var host: String = \"\",\n                @SerializedName(\"v4\")\n                var v4: String = \"\"\n            )\n        }\n\n        data class Services(\n            @SerializedName(\"http_proxy\")\n            var httpProxy: String = \"\"\n        )\n\n        data class Interface(\n            @SerializedName(\"addresses\")\n            var addresses: Addresses = Addresses()\n        ) {\n            data class Addresses(\n                @SerializedName(\"v6\")\n                var v6: String = \"\",\n                @SerializedName(\"v4\")\n                var v4: String = \"\"\n            )\n        }\n    }\n\n    data class Account(\n        @SerializedName(\"account_type\")\n        var accountType: String = \"\",\n        @SerializedName(\"role\")\n        var role: String = \"\",\n        @SerializedName(\"referral_renewal_countdown\")\n        var referralRenewalCountdown: Int = 0,\n        @SerializedName(\"created\")\n        var created: String = \"\",\n        @SerializedName(\"usage\")\n        var usage: Int = 0,\n        @SerializedName(\"warp_plus\")\n        var warpPlus: Boolean = false,\n        @SerializedName(\"referral_count\")\n        var referralCount: Int = 0,\n        @SerializedName(\"license\")\n        var license: String = \"\",\n        @SerializedName(\"quota\")\n        var quota: Int = 0,\n        @SerializedName(\"premium_data\")\n        var premiumData: Int = 0,\n        @SerializedName(\"id\")\n        var id: String = \"\",\n        @SerializedName(\"updated\")\n        var updated: String = \"\"\n    )\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git></contact-git>@sekai.icu>                  *\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 <http:></http:>//www.gnu.org/licenses/>.       *\n * *\n */\npackage io.nekohasekai.sagernet.utils.cf\n\nimport com.google.gson.Gson\nimport com.google.gson.annotations.SerializedName\nimport com.wireguard.crypto.Key\nimport java.text.SimpleDateFormat\nimport java.util.*\n\ndata class RegisterRequest(\n    @SerializedName(\"fcm_token\") var fcmToken: String = \"\",\n    @SerializedName(\"install_id\") var installedId: String = \"\",\n    var key: String = \"\",\n    var locale: String = \"\",\n    var model: String = \"\",\n    var tos: String = \"\",\n    var type: String = \"\"\n) {\n\n    companion object {\n        fun newRequest(publicKey: Key): String {\n            val request = RegisterRequest()\n            request.fcmToken = \"\"\n            request.installedId = \"\"\n            request.key = publicKey.toBase64()\n            request.locale = \"en_US\"\n            request.model = \"PC\"\n            val format = SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSS'000000'+08:00\", Locale.US)\n            request.tos = format.format(Date())\n            request.type = \"Android\"\n            return Gson().toJson(request)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt",
    "content": "/******************************************************************************\n * Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>                  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.utils.cf\n\nimport com.google.gson.Gson\n\ndata class UpdateDeviceRequest(\n    var name: String, var active: Boolean\n) {\n    companion object {\n        fun newRequest(name: String = \"SagerNet Client\", active: Boolean = true) =\n            Gson().toJson(UpdateDeviceRequest(name, active))\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/AppListPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.preference.Preference\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.utils.PackageCache\n\nclass AppListPreference : Preference {\n\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(\n        context, attrs, defStyle\n    )\n\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n    override fun getSummary(): CharSequence {\n        val packages = DataStore.routePackages.split(\"\\n\").filter { it.isNotBlank() }.map {\n            PackageCache.installedPackages[it]?.applicationInfo?.loadLabel(app.packageManager) ?: it\n        }\n        if (packages.isEmpty()) {\n            return context.getString(androidx.preference.R.string.not_set)\n        }\n        val count = packages.size\n        if (count <= 5) return packages.joinToString(\"\\n\")\n        return context.getString(R.string.apps_message, count)\n    }\n\n    fun postUpdate() {\n        notifyChanged()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/AutoCollapseTextView.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Rect\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport androidx.appcompat.widget.AppCompatTextView\nimport androidx.core.view.isGone\n\nclass AutoCollapseTextView @JvmOverloads constructor(\n    context: Context, attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) :\n    AppCompatTextView(context, attrs, defStyleAttr) {\n    override fun onTextChanged(\n        text: CharSequence?,\n        start: Int,\n        lengthBefore: Int,\n        lengthAfter: Int,\n    ) {\n        super.onTextChanged(text, start, lengthBefore, lengthAfter)\n        isGone = text.isNullOrEmpty()\n    }\n\n    // #1874\n    override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) =\n        try {\n            super.onFocusChanged(focused, direction, previouslyFocusedRect)\n        } catch (e: IndexOutOfBoundsException) {\n        }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onTouchEvent(event: MotionEvent?) = try {\n        super.onTouchEvent(event)\n    } catch (e: IndexOutOfBoundsException) {\n        false\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/ColorPickerPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.os.Parcel\nimport android.os.Parcelable\nimport android.os.Parcelable.Creator\nimport android.util.AttributeSet\nimport android.widget.ImageView\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.res.TypedArrayUtils\nimport androidx.preference.DialogPreference\nimport androidx.preference.PreferenceViewHolder\nimport com.takisoft.colorpicker.ColorPickerDialog\nimport com.takisoft.colorpicker.ColorStateDrawable\nimport com.takisoft.preferencex.PreferenceFragmentCompat\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ktx.app\n\nclass ColorPickerPreference @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet?,\n    defStyleAttr: Int,\n    defStyleRes: Int = 0\n) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {\n    companion object {\n        init {\n            PreferenceFragmentCompat.registerPreferenceFragment(\n                ColorPickerPreference::class.java,\n                ColorPickerPreferenceDialogFragmentCompat::class.java\n            )\n        }\n    }\n\n    init {\n        widgetLayoutResource = R.layout.preference_widget_color_swatch\n    }\n\n    val colors = app.resources.getIntArray(R.array.material_colors)\n    lateinit var colorDescriptions: Array<CharSequence>\n    private var colorIndex = 0\n    var columns = 0\n\n    @get:ColorPickerDialog.Size\n    var size = 0\n    var isSortColors = false\n    private var colorWidget: ImageView? = null\n\n    @SuppressLint(\"RestrictedApi\")\n    constructor(context: Context, attrs: AttributeSet?) : this(\n        context, attrs, TypedArrayUtils.getAttr(\n            context, R.attr.dialogPreferenceStyle,\n            android.R.attr.dialogPreferenceStyle\n        )\n    )\n\n    constructor(context: Context) : this(context, null)\n\n    override fun onBindViewHolder(holder: PreferenceViewHolder) {\n        super.onBindViewHolder(holder)\n        colorWidget = holder.findViewById(R.id.color_picker_widget) as ImageView\n        setColorOnWidget(colors[colorIndex])\n    }\n\n    private fun setColorOnWidget(color: Int) {\n        if (colorWidget == null) {\n            return\n        }\n        val colorDrawable = arrayOf(\n            ContextCompat.getDrawable(\n                context, R.drawable.colorpickerpreference_pref_swatch\n            )\n        )\n        colorWidget!!.setImageDrawable(ColorStateDrawable(colorDrawable, color))\n    }\n\n    /**\n     * Returns the current color.\n     *\n     * @return The current color.\n     */\n    fun getColor(): Int {\n        return colorIndex\n    }\n\n    fun setColor(colorIndex: Int) {\n        setInternalColor(colors.indexOfFirst { it == colorIndex }, false)\n    }\n\n    private fun setInternalColor(colorIndexToSet: Int, force: Boolean) {\n        val colorIndex = if (colorIndexToSet >= colors.size || colorIndexToSet < 0) colors.size - 1 else colorIndexToSet\n        val oldColor = getPersistedInt(colors.size) - 1\n        val changed = oldColor != colorIndex\n        if (changed || force) {\n            this.colorIndex = colorIndex\n            persistInt(colorIndex + 1)\n            setColorOnWidget(colors[colorIndex])\n            notifyChanged()\n        }\n    }\n\n    override fun onSetInitialValue(defaultValueObj: Any?) {\n        setInternalColor(\n            getPersistedInt(\n                colors.size\n            ) - 1, true\n        )\n    }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val superState = super.onSaveInstanceState()\n        if (isPersistent) { // No need to save instance state since it's persistent\n            return superState\n        }\n        val myState = SavedState(superState)\n        myState.color = colorIndex\n        return myState\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        if (state.javaClass != SavedState::class.java) { // Didn't save state for us in onSaveInstanceState\n            super.onRestoreInstanceState(state)\n            return\n        }\n        val myState = state as SavedState\n        super.onRestoreInstanceState(myState.superState)\n        colorIndex = myState.color\n    }\n\n    private class SavedState : BaseSavedState {\n        var color = 0\n\n        constructor(source: Parcel) : super(source) {\n            color = source.readInt()\n        }\n\n        constructor(superState: Parcelable?) : super(superState) {}\n\n        override fun writeToParcel(dest: Parcel, flags: Int) {\n            super.writeToParcel(dest, flags)\n            dest.writeInt(color)\n        }\n\n        companion object {\n            @JvmField\n            val CREATOR: Creator<SavedState> = object : Creator<SavedState> {\n                override fun createFromParcel(`in`: Parcel): SavedState {\n                    return SavedState(`in`)\n                }\n\n                override fun newArray(size: Int): Array<SavedState?> {\n                    return arrayOfNulls(size)\n                }\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/ColorPickerPreferenceDialogFragmentCompat.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.app.Dialog\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport androidx.preference.PreferenceDialogFragmentCompat\nimport com.takisoft.colorpicker.ColorPickerDialog\nimport com.takisoft.colorpicker.OnColorSelectedListener\n\nclass ColorPickerPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat(),\n    OnColorSelectedListener {\n    private var pickedColor = 0\n    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\n        val pref = colorPickerPreference\n        val params = ColorPickerDialog.Params.Builder(context)\n            .setSelectedColor(pref.getColor())\n            .setColors(pref.colors)\n//            .setColorContentDescriptions(pref.colorDescriptions)\n            .setSize(pref.size)\n            .setSortColors(pref.isSortColors)\n            .setColumns(pref.columns)\n            .build()\n        val dialog = ColorPickerDialog(requireContext(), this, params)\n        dialog.setTitle(pref.dialogTitle)\n        return dialog\n    }\n\n    override fun onDialogClosed(positiveResult: Boolean) {\n        val preference = colorPickerPreference\n        if (positiveResult && preference.callChangeListener(pickedColor)) {\n            preference.setColor(pickedColor)\n        }\n    }\n\n    override fun onColorSelected(color: Int) {\n        pickedColor = color\n        super.onClick(dialog, DialogInterface.BUTTON_POSITIVE)\n    }\n\n    val colorPickerPreference: ColorPickerPreference\n        get() = preference as ColorPickerPreference\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/EditConfigPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.content.Intent\nimport android.util.AttributeSet\nimport androidx.preference.Preference\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ui.profile.ConfigEditActivity\n\nclass EditConfigPreference : Preference {\n\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context, attrs, defStyleAttr\n    )\n\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?) : super(context)\n\n    init {\n        intent = Intent(context, ConfigEditActivity::class.java)\n    }\n\n    override fun getSummary(): CharSequence {\n        val config = DataStore.serverConfig\n        return if (DataStore.serverConfig.isBlank()) {\n            return app.resources.getString(androidx.preference.R.string.not_set)\n        } else {\n            app.resources.getString(R.string.lines, config.split('\\n').size)\n        }\n    }\n\n    public override fun notifyChanged() {\n        super.notifyChanged()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/FabProgressBehavior.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.coordinatorlayout.widget.CoordinatorLayout\nimport com.google.android.material.progressindicator.CircularProgressIndicator\n\nclass FabProgressBehavior(context: Context, attrs: AttributeSet?) :\n    CoordinatorLayout.Behavior<CircularProgressIndicator>(context, attrs) {\n    override fun layoutDependsOn(\n        parent: CoordinatorLayout,\n        child: CircularProgressIndicator,\n        dependency: View,\n    ): Boolean {\n        return dependency.id == (child.layoutParams as CoordinatorLayout.LayoutParams).anchorId\n    }\n\n    override fun onLayoutChild(\n        parent: CoordinatorLayout, child: CircularProgressIndicator,\n        layoutDirection: Int,\n    ): Boolean {\n        val size = parent.getDependencies(child).single().measuredHeight + child.trackThickness\n        return if (child.indicatorSize != size) {\n            child.indicatorSize = size\n            true\n        } else false\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/GroupPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.database.SagerDatabase\n\nclass GroupPreference : SimpleMenuPreference {\n\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(\n        context, attrs, defStyle\n    )\n\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n    init {\n        val groups = SagerDatabase.groupDao.allGroups()\n\n        entries = groups.map { it.displayName() }.toTypedArray()\n        entryValues = groups.map { \"${it.id}\" }.toTypedArray()\n    }\n\n    override fun getSummary(): CharSequence {\n        if (value != \"0\") {\n            return SagerDatabase.groupDao.getById(value.toLong())?.displayName() ?: super.getSummary()\n        }\n        return super.getSummary()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.net.Uri\nimport android.util.AttributeSet\nimport androidx.core.widget.addTextChangedListener\nimport com.google.android.material.textfield.TextInputLayout\nimport com.takisoft.preferencex.EditTextPreference\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nclass LinkOrContentPreference : EditTextPreference {\n\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context, attrs, defStyleAttr\n    )\n\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n\n    init {\n        dialogLayoutResource = R.layout.layout_link_dialog\n\n        setOnBindEditTextListener {\n            val linkLayout = it.rootView.findViewById<TextInputLayout>(R.id.input_layout)\n            fun validate() {\n                val link = it.text\n                if (link.isBlank()) {\n                    linkLayout.isErrorEnabled = false\n                    return\n                }\n\n                try {\n                    if (Uri.parse(link.toString()).scheme == \"content\") {\n                        linkLayout.isErrorEnabled = false\n                        return\n                    }\n                    val url = link.toString().toHttpUrl()\n                    if (\"http\".equals(url.scheme, true)) {\n                        linkLayout.error = app.getString(R.string.cleartext_http_warning)\n                        linkLayout.isErrorEnabled = true\n                    } else {\n                        linkLayout.isErrorEnabled = false\n                    }\n                } catch (e: Exception) {\n                    linkLayout.error = e.readableMessage\n                    linkLayout.isErrorEnabled = true\n                }\n\n            }\n            validate()\n            it.addTextChangedListener {\n                validate()\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/LinkPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.core.widget.addTextChangedListener\nimport com.google.android.material.textfield.TextInputLayout\nimport com.takisoft.preferencex.EditTextPreference\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ktx.app\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nclass LinkPreference : EditTextPreference {\n\n    var defaultValue: String? = null\n\n    constructor(context: Context) : this(context, null)\n\n    constructor(\n        context: Context,\n        attrs: AttributeSet?,\n    ) : this(context, attrs, com.takisoft.preferencex.R.attr.editTextPreferenceStyle)\n\n    constructor(\n        context: Context,\n        attrs: AttributeSet?,\n        defStyleAttr: Int,\n    ) : this(context, attrs, defStyleAttr, 0)\n\n    constructor(\n        context: Context,\n        attrs: AttributeSet?,\n        defStyleAttr: Int,\n        defStyleRes: Int,\n    ) : super(context, attrs, defStyleAttr, defStyleRes) {\n        val a = context.obtainStyledAttributes(\n            attrs, R.styleable.Preference, defStyleAttr, defStyleRes\n        )\n        if (a.hasValue(androidx.preference.R.styleable.Preference_defaultValue)) {\n            defaultValue = onGetDefaultValue(\n                a, androidx.preference.R.styleable.Preference_defaultValue\n            )?.toString()\n        } else if (a.hasValue(androidx.preference.R.styleable.Preference_android_defaultValue)) {\n            defaultValue = onGetDefaultValue(\n                a, androidx.preference.R.styleable.Preference_android_defaultValue\n            )?.toString()\n        }\n    }\n\n    init {\n        dialogLayoutResource = R.layout.layout_link_dialog\n\n        setOnBindEditTextListener {\n            val linkLayout = it.rootView.findViewById<TextInputLayout>(R.id.input_layout)\n            fun validate() {\n                val link = it.text\n                if (link.isBlank()) {\n                    linkLayout.isErrorEnabled = false\n                    return\n                }\n                try {\n                    val url = link.toString().toHttpUrl()\n                    if (\"http\".equals(url.scheme, true)) {\n                        linkLayout.error = app.getString(R.string.cleartext_http_warning)\n                        linkLayout.isErrorEnabled = true\n                    } else {\n                        linkLayout.isErrorEnabled = false\n                    }\n                } catch (e: Exception) {\n                    linkLayout.error = e.readableMessage\n                    linkLayout.isErrorEnabled = true\n                }\n            }\n            validate()\n            it.addTextChangedListener {\n                validate()\n            }\n        }\n\n        setOnPreferenceChangeListener { _, newValue ->\n            if ((newValue as String).isBlank()) {\n                text = defaultValue\n                false\n            } else try {\n                newValue.toHttpUrl()\n                true\n            } catch (ignored: Exception) {\n                false\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/OOCv1TokenPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.core.widget.addTextChangedListener\nimport cn.hutool.core.util.CharUtil\nimport cn.hutool.json.JSONObject\nimport com.google.android.material.textfield.TextInputLayout\nimport com.takisoft.preferencex.EditTextPreference\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nclass OOCv1TokenPreference : EditTextPreference {\n\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int\n    ) : super(context, attrs, defStyleAttr)\n\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n\n    init {\n        dialogLayoutResource = R.layout.layout_link_dialog\n\n        setOnBindEditTextListener { editText ->\n            editText.isSingleLine = false\n            val linkLayout = editText.rootView.findViewById<TextInputLayout>(R.id.input_layout)\n\n            fun validate() {\n                if (editText.text.isBlank()) {\n                    linkLayout.isErrorEnabled = false\n                    return\n                }\n                var isValid = true\n                try {\n                    val tokenObject = JSONObject(editText.text)\n                    val version = tokenObject.getInt(\"version\")\n                    if (version != 1) {\n                        isValid = false\n                        if (version != null) {\n                            linkLayout.error = \"Unsupported OOC version $version\"\n                        } else {\n                            linkLayout.error = \"Missing field: version\"\n                        }\n                    }\n                    if (isValid) {\n                        val baseUrl = tokenObject.getStr(\"baseUrl\")\n                        when {\n                            baseUrl.isNullOrBlank() -> {\n                                linkLayout.error = \"Missing field: baseUrl\"\n                                isValid = false\n                            }\n                            baseUrl.endsWith(\"/\") -> {\n                                linkLayout.error = \"baseUrl must not contain a trailing slash\"\n                                isValid = false\n                            }\n                            !baseUrl.startsWith(\"https://\") -> {\n                                isValid = false\n                                linkLayout.error = \"Protocol scheme must be https\"\n                            }\n                            else -> try {\n                                baseUrl.toHttpUrl()\n                            } catch (e: Exception) {\n                                isValid = false\n                                linkLayout.error = e.readableMessage\n                            }\n                        }\n                    }\n                    if (isValid && tokenObject.getStr(\"secret\").isNullOrBlank()) {\n                        isValid = false\n                        linkLayout.error = \"Missing field: secret\"\n                    }\n                    if (isValid && tokenObject.getStr(\"userId\").isNullOrBlank()) {\n                        isValid = false\n                        linkLayout.error = \"Missing field: userId\"\n                    }\n                    if (isValid) {\n                        val certSha256 = tokenObject.getStr(\"certSha256\")\n                        if (!certSha256.isNullOrBlank()) {\n                            when {\n                                certSha256.length != 64 -> {\n                                    isValid = false\n                                    linkLayout.error =\n                                        \"certSha256 must be a SHA-256 hexadecimal string\"\n                                }\n                                !certSha256.all { CharUtil.isLetterLower(it) || CharUtil.isNumber(it) } -> {\n                                    isValid = false\n                                    linkLayout.error =\n                                        \"certSha256 must be a hexadecimal string with lowercase letters\"\n                                }\n                            }\n                        }\n                    }\n                } catch (e: Exception) {\n                    isValid = false\n                    linkLayout.error = e.readableMessage\n                }\n\n                linkLayout.isErrorEnabled = !isValid\n            }\n            validate()\n            editText.addTextChangedListener {\n                validate()\n            }\n        }\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.takisoft.preferencex.SimpleMenuPreference\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.database.ProfileManager\n\nclass OutboundPreference : SimpleMenuPreference {\n\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(\n        context,\n        attrs,\n        defStyle\n    )\n\n    constructor(\n        context: Context?,\n        attrs: AttributeSet?,\n        defStyleAttr: Int,\n        defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n    init {\n        setEntries(R.array.outbound_entry)\n        setEntryValues(R.array.outbound_value)\n    }\n\n    override fun getSummary(): CharSequence {\n        if (value == \"3\") {\n            val routeOutbound = DataStore.routeOutboundRule\n            if (routeOutbound > 0) {\n                ProfileManager.getProfile(routeOutbound)?.displayName()?.let {\n                    return it\n                }\n            }\n        }\n        return super.getSummary()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/QRCodeDialog.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.graphics.Bitmap\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport androidx.core.os.bundleOf\nimport androidx.fragment.app.DialogFragment\nimport com.google.zxing.BarcodeFormat\nimport com.google.zxing.EncodeHintType\nimport com.google.zxing.MultiFormatWriter\nimport com.google.zxing.WriterException\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ktx.Logs\nimport io.nekohasekai.sagernet.ktx.readableMessage\nimport io.nekohasekai.sagernet.ui.MainActivity\nimport java.nio.charset.StandardCharsets\n\nclass QRCodeDialog() : DialogFragment() {\n\n    companion object {\n        private const val KEY_URL = \"io.nekohasekai.sagernet.QRCodeDialog.KEY_URL\"\n        private val iso88591 = StandardCharsets.ISO_8859_1.newEncoder()\n    }\n\n    constructor(url: String) : this() {\n        arguments = bundleOf(Pair(KEY_URL, url))\n    }\n\n    /**\n     * Based on:\n     * https://android.googlesource.com/platform/\npackages/apps/Settings/+/0d706f0/src/com/android/settings/wifi/qrcode/QrCodeGenerator.java\n     * https://android.googlesource.com/platform/\npackages/apps/Settings/+/8a9ccfd/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java#153\n     */\n    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = try {\n        val url = arguments?.getString(KEY_URL)!!\n        val size = resources.getDimensionPixelSize(R.dimen.qrcode_size)\n        val hints = mutableMapOf<EncodeHintType, Any>()\n        if (!iso88591.canEncode(url)) hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8.name()\n        val qrBits = MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, size, size, hints)\n        ImageView(context).apply {\n            layoutParams = ViewGroup.LayoutParams(size, size)\n            setImageBitmap(Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565).apply {\n                for (x in 0 until size) for (y in 0 until size) {\n                    setPixel(x, y, if (qrBits.get(x, y)) Color.BLACK else Color.WHITE)\n                }\n            })\n        }\n    } catch (e: WriterException) {\n        Logs.w(e)\n        (activity as MainActivity).snackbar(e.readableMessage).show()\n        dismiss()\n        null\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.os.Build\nimport android.util.AttributeSet\nimport android.view.PointerIcon\nimport android.view.View\nimport androidx.annotation.DrawableRes\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.dynamicanimation.animation.DynamicAnimation\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.lifecycleScope\nimport androidx.vectordrawable.graphics.drawable.Animatable2Compat\nimport androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.google.android.material.progressindicator.BaseProgressIndicator\nimport com.google.android.material.progressindicator.DeterminateDrawable\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.ktx.getColorAttr\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport java.util.*\n\nclass ServiceButton @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) :\n    FloatingActionButton(context, attrs, defStyleAttr), DynamicAnimation.OnAnimationEndListener {\n    companion object {\n        private val springAnimator by lazy {\n            DeterminateDrawable::class.java.getDeclaredField(\"springAnimator\")\n                .apply { isAccessible = true }\n        }\n    }\n\n    private val callback = object : Animatable2Compat.AnimationCallback() {\n        override fun onAnimationEnd(drawable: Drawable) {\n            super.onAnimationEnd(drawable)\n            var next = animationQueue.peek() ?: return\n            if (next.icon.current == drawable) {\n                animationQueue.pop()\n                next = animationQueue.peek() ?: return\n            }\n            next.start()\n        }\n    }\n\n    private inner class AnimatedState(\n        @DrawableRes resId: Int,\n        private val onStart: BaseProgressIndicator<*>.() -> Unit = { hideProgress() },\n    ) {\n        val icon: AnimatedVectorDrawableCompat =\n            AnimatedVectorDrawableCompat.create(context, resId)!!.apply {\n                registerAnimationCallback(this@ServiceButton.callback)\n            }\n\n        fun start() {\n            setImageDrawable(icon)\n            setColorFilter(context.getColorAttr(R.attr.whiteOrTextPrimary))\n            icon.start()\n            progress.onStart()\n        }\n\n        fun stop() = icon.stop()\n    }\n\n    private val iconStopped by lazy { AnimatedState(R.drawable.ic_service_stopped) }\n    private val iconConnecting by lazy {\n        AnimatedState(R.drawable.ic_service_connecting) {\n            hideProgress()\n            delayedAnimation = (context as LifecycleOwner).lifecycleScope.launchWhenStarted {\n                delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 100L)\n                isIndeterminate = true\n                show()\n            }\n        }\n    }\n    private val iconConnected by lazy {\n        AnimatedState(R.drawable.ic_service_connected) {\n            delayedAnimation?.cancel()\n            setProgressCompat(1, true)\n        }\n    }\n    private val iconStopping by lazy { AnimatedState(R.drawable.ic_service_stopping) }\n    private val animationQueue = ArrayDeque<AnimatedState>()\n\n    private var checked = false\n    private var delayedAnimation: Job? = null\n    private lateinit var progress: BaseProgressIndicator<*>\n    fun initProgress(progress: BaseProgressIndicator<*>) {\n        this.progress = progress\n        (springAnimator.get(progress.progressDrawable) as DynamicAnimation<*>).addEndListener(this)\n    }\n\n    override fun onAnimationEnd(\n        animation: DynamicAnimation<out DynamicAnimation<*>>?, canceled: Boolean, value: Float,\n        velocity: Float,\n    ) {\n        if (!canceled) progress.hide()\n    }\n\n    private fun hideProgress() {\n        delayedAnimation?.cancel()\n        progress.hide()\n    }\n\n    override fun onCreateDrawableState(extraSpace: Int): IntArray {\n        val drawableState = super.onCreateDrawableState(extraSpace + 1)\n        if (checked) View.mergeDrawableStates(drawableState, intArrayOf(android.R.attr.state_checked))\n        return drawableState\n    }\n\n    fun changeState(state: BaseService.State, previousState: BaseService.State, animate: Boolean) {\n        when (state) {\n            BaseService.State.Connecting -> changeState(iconConnecting, animate)\n            BaseService.State.Connected -> changeState(iconConnected, animate)\n            BaseService.State.Stopping -> {\n                changeState(iconStopping, animate && previousState == BaseService.State.Connected)\n            }\n            else -> changeState(iconStopped, animate)\n        }\n        checked = state == BaseService.State.Connected\n        refreshDrawableState()\n        val description = context.getText(if (state.canStop) R.string.stop else R.string.connect)\n        contentDescription = description\n        TooltipCompat.setTooltipText(this, description)\n        val enabled = state.canStop || state == BaseService.State.Stopped\n        isEnabled = enabled\n        if (Build.VERSION.SDK_INT >= 24) pointerIcon = PointerIcon.getSystemIcon(context,\n            if (enabled) PointerIcon.TYPE_HAND else PointerIcon.TYPE_WAIT)\n    }\n\n    private fun changeState(icon: AnimatedState, animate: Boolean) {\n        fun counters(a: AnimatedState, b: AnimatedState): Boolean =\n            a == iconStopped && b == iconConnecting ||\n                    a == iconConnecting && b == iconStopped ||\n                    a == iconConnected && b == iconStopping ||\n                    a == iconStopping && b == iconConnected\n        if (animate) {\n            if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {\n                animationQueue.add(icon)\n                if (animationQueue.size == 1) icon.start()\n            } else animationQueue.removeLast()\n        } else {\n            animationQueue.peekFirst()?.stop()\n            animationQueue.clear()\n            icon.start()    // force ensureAnimatorSet to be called so that stop() will work\n            icon.stop()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.text.format.Formatter\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.TextView\nimport androidx.appcompat.widget.TooltipCompat\nimport androidx.coordinatorlayout.widget.CoordinatorLayout\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.whenStarted\nimport com.google.android.material.bottomappbar.BottomAppBar\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.bg.BaseService\nimport io.nekohasekai.sagernet.database.DataStore\nimport io.nekohasekai.sagernet.ktx.*\nimport io.nekohasekai.sagernet.ui.MainActivity\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\nclass StatsBar @JvmOverloads constructor(\n    context: Context, attrs: AttributeSet? = null,\n    defStyleAttr: Int = R.attr.bottomAppBarStyle,\n) : BottomAppBar(context, attrs, defStyleAttr) {\n    private lateinit var statusText: TextView\n    private lateinit var txText: TextView\n    private lateinit var rxText: TextView\n    private lateinit var behavior: YourBehavior\n    override fun getBehavior(): YourBehavior {\n        if (!this::behavior.isInitialized) behavior = YourBehavior()\n        return behavior\n    }\n\n    class YourBehavior : Behavior() {\n        override fun onNestedScroll(\n            coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,\n            dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,\n            type: Int, consumed: IntArray,\n        ) {\n            super.onNestedScroll(\n                coordinatorLayout,\n                child,\n                target,\n                dxConsumed,\n                dyConsumed + dyUnconsumed,\n                dxUnconsumed,\n                0,\n                type,\n                consumed\n            )\n        }\n\n        var hide = false\n\n        override fun slideUp(child: BottomAppBar) {\n            hide = false\n            super.slideUp(child)\n        }\n\n        override fun slideDown(child: BottomAppBar) {\n            hide = true\n            super.slideDown(child)\n        }\n    }\n\n\n    override fun setOnClickListener(l: OnClickListener?) {\n        statusText = findViewById(R.id.status)\n        txText = findViewById(R.id.tx)\n        rxText = findViewById(R.id.rx)\n        super.setOnClickListener(l)\n    }\n\n    private fun setStatus(text: CharSequence) {\n        statusText.text = text\n        TooltipCompat.setTooltipText(this, text)\n    }\n\n    fun changeState(state: BaseService.State) {\n        val activity = context as MainActivity\n        fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) {\n            delay(100L)\n            activity.whenStarted { what() }\n        }\n        if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {\n            postWhenStarted {\n                performShow()\n                setStatus(app.getText(R.string.vpn_connected))\n            }\n        } else {\n            postWhenStarted {\n                performHide()\n            }\n            updateTraffic(0, 0)\n            setStatus(\n                context.getText(\n                    when (state) {\n                        BaseService.State.Connecting -> R.string.connecting\n                        BaseService.State.Stopping -> R.string.stopping\n                        else -> R.string.not_connected\n                    }\n                )\n            )\n        }\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    fun updateTraffic(txRate: Long, rxRate: Long) {\n        txText.text = \"▲  ${\n            context.getString(\n                R.string.speed, Formatter.formatFileSize(context, txRate)\n            )\n        }\"\n        rxText.text = \"▼  ${\n            context.getString(\n                R.string.speed, Formatter.formatFileSize(context, rxRate)\n            )\n        }\"\n    }\n\n    fun testConnection() {\n        val activity = context as MainActivity\n        isEnabled = false\n        setStatus(app.getText(R.string.connection_test_testing))\n        runOnDefaultDispatcher {\n            try {\n                val elapsed = activity.urlTest()\n                onMainDispatcher {\n                    isEnabled = true\n                    setStatus(\n                        app.getString(\n                            if (DataStore.connectionTestURL.startsWith(\"https://\")) {\n                                R.string.connection_test_available\n                            } else {\n                                R.string.connection_test_available_http\n                            }, elapsed\n                        )\n                    )\n                }\n\n            } catch (e: Exception) {\n                Logs.w(e)\n                onMainDispatcher {\n                    isEnabled = true\n                    setStatus(app.getText(R.string.connection_test_testing))\n\n                    activity.snackbar(\n                        app.getString(\n                            R.string.connection_test_error, e.readableMessage\n                        )\n                    ).show()\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/UndoSnackbarManager.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport com.google.android.material.snackbar.Snackbar\nimport io.nekohasekai.sagernet.R\nimport io.nekohasekai.sagernet.ui.ThemedActivity\n\n/**\n * @param activity ThemedActivity.\n * //@param view The view to find a parent from.\n * @param undo Callback for undoing removals.\n * @param commit Callback for committing removals.\n * @tparam T Item type.\n */\nclass UndoSnackbarManager<in T>(\n    private val activity: ThemedActivity,\n    private val callback: Interface<T>,\n) {\n\n    interface Interface<in T> {\n        fun undo(actions: List<Pair<Int, T>>)\n        fun commit(actions: List<Pair<Int, T>>)\n    }\n\n    private val recycleBin = ArrayList<Pair<Int, T>>()\n    private val removedCallback = object : Snackbar.Callback() {\n        override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {\n            if (last === transientBottomBar && event != DISMISS_EVENT_ACTION) {\n                callback.commit(recycleBin)\n                recycleBin.clear()\n                last = null\n            }\n        }\n    }\n\n    private var last: Snackbar? = null\n\n    fun remove(items: Collection<Pair<Int, T>>) {\n        recycleBin.addAll(items)\n        val count = recycleBin.size\n        activity.snackbar(activity.resources.getQuantityString(R.plurals.removed, count, count))\n            .apply {\n                addCallback(removedCallback)\n                setAction(R.string.undo) {\n                    callback.undo(recycleBin.reversed())\n                    recycleBin.clear()\n                }\n                last = this\n                show()\n            }\n    }\n\n    fun remove(vararg items: Pair<Int, T>) = remove(items.toList())\n\n    fun flush() = last?.dismiss()\n}\n"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/UserAgentPreference.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.takisoft.preferencex.EditTextPreference\nimport io.nekohasekai.sagernet.ktx.USER_AGENT\nimport io.nekohasekai.sagernet.ktx.USER_AGENT_ORIGIN\n\nclass UserAgentPreference : EditTextPreference {\n\n    constructor(context: Context?) : super(context)\n    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)\n    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(\n        context, attrs, defStyle\n    )\n\n    constructor(\n        context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int\n    ) : super(context, attrs, defStyleAttr, defStyleRes)\n\n    var isOOCv1 = false\n\n    public override fun notifyChanged() {\n        super.notifyChanged()\n    }\n\n    override fun getSummary(): CharSequence {\n        if (text.isNullOrBlank()) {\n            return if (isOOCv1) USER_AGENT_ORIGIN else USER_AGENT\n        }\n        return super.getSummary()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/io/nekohasekai/sagernet/widget/WindowInsetsListeners.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.widget\n\nimport android.view.View\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.graphics.Insets\nimport androidx.core.view.*\nimport io.nekohasekai.sagernet.R\n\nobject ListHolderListener : OnApplyWindowInsetsListener {\n    override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {\n        val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())\n        view.setPadding(statusBarInsets.left,\n            statusBarInsets.top,\n            statusBarInsets.right,\n            statusBarInsets.bottom)\n        return WindowInsetsCompat.Builder(insets).apply {\n            setInsets(WindowInsetsCompat.Type.statusBars(), Insets.NONE)\n            /*setInsets(WindowInsetsCompat.Type.navigationBars(),\n                insets.getInsets(WindowInsetsCompat.Type.navigationBars()))*/\n        }.build()\n    }\n\n    fun setup(activity: AppCompatActivity) = activity.findViewById<View>(android.R.id.content).let {\n        ViewCompat.setOnApplyWindowInsetsListener(it, ListHolderListener)\n        WindowCompat.setDecorFitsSystemWindows(activity.window, false)\n    }\n}\n\nobject MainListListener : OnApplyWindowInsetsListener {\n    override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat) = insets.apply {\n        view.updatePadding(bottom = view.resources.getDimensionPixelOffset(R.dimen.main_list_padding_bottom) +\n                insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom)\n    }\n}\n\nobject ListListener : OnApplyWindowInsetsListener {\n    override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat) = insets.apply {\n        view.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom)\n    }\n}\n"
  },
  {
    "path": "app/src/main/play/release-notes/en-US/default.txt",
    "content": "For changelog, please check messages in the telegram update channel ( https://t.me/AnXray )."
  },
  {
    "path": "app/src/main/play/release-notes/zh-CN/default.txt",
    "content": "由于字数限制, 请至 Telegram 更新频道 https://t.me/AnXray 查看日志."
  },
  {
    "path": "app/src/main/res/color/chip_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- 24% opacity -->\n    <item android:alpha=\"0.24\" android:color=\"?attr/primaryOrTextPrimary\" android:state_enabled=\"true\" android:state_selected=\"true\" />\n    <item android:alpha=\"0.24\" android:color=\"?attr/primaryOrTextPrimary\" android:state_checked=\"true\" android:state_enabled=\"true\" />\n    <!-- 12% of 87% opacity -->\n    <item android:alpha=\"0.10\" android:color=\"?attr/colorOnSurface\" android:state_enabled=\"true\" />\n    <item android:alpha=\"0.12\" android:color=\"?attr/colorOnSurface\" />\n\n</selector>\n"
  },
  {
    "path": "app/src/main/res/color/chip_ripple_color.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:alpha=\"@dimen/mtrl_low_ripple_pressed_alpha\" android:color=\"?attr/primaryOrTextPrimary\" android:state_pressed=\"true\" />\n    <item android:alpha=\"@dimen/mtrl_low_ripple_focused_alpha\" android:color=\"?attr/colorOnSurface\" android:state_focused=\"true\" android:state_hovered=\"true\" />\n    <item android:alpha=\"@dimen/mtrl_low_ripple_focused_alpha\" android:color=\"?attr/colorOnSurface\" android:state_focused=\"true\" />\n    <item android:alpha=\"@dimen/mtrl_low_ripple_hovered_alpha\" android:color=\"?attr/colorOnSurface\" android:state_hovered=\"true\" />\n    <item android:alpha=\"@dimen/mtrl_low_ripple_default_alpha\" android:color=\"?attr/colorOnSurface\" />\n\n</selector>\n"
  },
  {
    "path": "app/src/main/res/color/chip_text_color.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:color=\"?attr/primaryOrTextPrimary\" android:state_enabled=\"true\" android:state_selected=\"true\" />\n    <item android:color=\"?attr/primaryOrTextPrimary\" android:state_checked=\"true\" android:state_enabled=\"true\" />\n    <!-- 87% opacity. -->\n    <item android:alpha=\"0.87\" android:color=\"?attr/colorOnSurface\" android:state_enabled=\"true\" />\n    <!-- 38% of 87% opacity. -->\n    <item android:alpha=\"0.33\" android:color=\"?attr/colorOnSurface\" />\n\n</selector>\n"
  },
  {
    "path": "app/src/main/res/color/navigation_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"?attr/primaryOrTextSecondary\" android:state_checked=\"true\" />\n    <item android:color=\"?android:textColorPrimary\" android:state_checked=\"false\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/color/navigation_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"?itemShapeFillColor\" android:state_checked=\"true\" />\n    <item android:color=\"@android:color/transparent\" android:state_checked=\"false\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/baseline_construction_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M13.7827,15.1719l2.1213,-2.1213l5.9962,5.9962l-2.1213,2.1213z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M17.5,10c1.93,0 3.5,-1.57 3.5,-3.5c0,-0.58 -0.16,-1.12 -0.41,-1.6l-2.7,2.7L16.4,6.11l2.7,-2.7C18.62,3.16 18.08,3 17.5,3C15.57,3 14,4.57 14,6.5c0,0.41 0.08,0.8 0.21,1.16l-1.85,1.85l-1.78,-1.78l0.71,-0.71L9.88,5.61L12,3.49c-1.17,-1.17 -3.07,-1.17 -4.24,0L4.22,7.03l1.41,1.41H2.81L2.1,9.15l3.54,3.54l0.71,-0.71V9.15l1.41,1.41l0.71,-0.71l1.78,1.78l-7.41,7.41l2.12,2.12L16.34,9.79C16.7,9.92 17.09,10 17.5,10z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/baseline_delete_sweep_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2L13,8L3,8v10zM14,5h-3l-1,-1L6,4L5,5L2,5v2h12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/baseline_save_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/baseline_send_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/baseline_translate_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/baseline_wrap_text_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4,19h6v-2L4,17v2zM20,5L4,5v2h16L20,5zM17,11L4,11v2h13.25c1.1,0 2,0.9 2,2s-0.9,2 -2,2L15,17v-2l-3,3 3,3v-2h2c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_copyright.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M10.08,10.86c0.05,-0.33 0.16,-0.62 0.3,-0.87s0.34,-0.46 0.59,-0.62c0.24,-0.15 0.54,-0.22 0.91,-0.23 0.23,0.01 0.44,0.05 0.63,0.13 0.2,0.09 0.38,0.21 0.52,0.36s0.25,0.33 0.34,0.53 0.13,0.42 0.14,0.64h1.79c-0.02,-0.47 -0.11,-0.9 -0.28,-1.29s-0.4,-0.73 -0.7,-1.01 -0.66,-0.5 -1.08,-0.66 -0.88,-0.23 -1.39,-0.23c-0.65,0 -1.22,0.11 -1.7,0.34s-0.88,0.53 -1.2,0.92 -0.56,0.84 -0.71,1.36S8,11.29 8,11.87v0.27c0,0.58 0.08,1.12 0.23,1.64s0.39,0.97 0.71,1.35 0.72,0.69 1.2,0.91 1.05,0.34 1.7,0.34c0.47,0 0.91,-0.08 1.32,-0.23s0.77,-0.36 1.08,-0.63 0.56,-0.58 0.74,-0.94 0.29,-0.74 0.3,-1.15h-1.79c-0.01,0.21 -0.06,0.4 -0.15,0.58s-0.21,0.33 -0.36,0.46 -0.32,0.23 -0.52,0.3c-0.19,0.07 -0.39,0.09 -0.6,0.1 -0.36,-0.01 -0.66,-0.08 -0.89,-0.23 -0.25,-0.16 -0.45,-0.37 -0.59,-0.62s-0.25,-0.55 -0.3,-0.88 -0.08,-0.67 -0.08,-1v-0.27c0,-0.35 0.03,-0.68 0.08,-1.01zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_delete.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n            android:fillColor=\"#FFFFFFFF\"\n            android:pathData=\"M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_description.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:autoMirrored=\"true\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_dns.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M20,13H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-6c0,-0.55 -0.45,-1 -1,-1zM7,19c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM20,3H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1V4c0,-0.55 -0.45,-1 -1,-1zM7,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_done.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n            android:fillColor=\"#FF000000\"\n            android:pathData=\"M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_lock.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_lock_open.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_note_add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:autoMirrored=\"true\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,16h-3v3h-2v-3L8,16v-2h3v-3h2v3h3v2zM13,9L13,3.5L18.5,9L13,9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_action_settings.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_app_shortcut_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  Copyright 2017 Google Inc.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n  in compliance with the License. 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 distributed under the License\n  is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n  or implied. See the License for the specific language governing permissions and limitations under\n  the License.\n  -->\n\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@color/material_grey_100\" />\n    <size android:width=\"44dp\" android:height=\"44dp\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/ic_av_playlist_add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:autoMirrored=\"true\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_add_road_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,18l0,-3l-2,0l0,3l-3,0l0,2l3,0l0,3l2,0l0,-3l3,0l0,-2z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M18,4h2v9h-2z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4,4h2v16h-2z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11,4h2v4h-2z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11,10h2v4h-2z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11,16h2v4h-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_airplanemode_active_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M22,16v-2l-8.5,-5V3.5C13.5,2.67 12.83,2 12,2s-1.5,0.67 -1.5,1.5V9L2,14v2l8.5,-2.5V19L8,20.5L8,22l4,-1l4,1l0,-1.5L13.5,19v-5.5L22,16z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_bug_report_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_camera_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_card_giftcard_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,6h-2.18c0.11,-0.31 0.18,-0.65 0.18,-1 0,-1.66 -1.34,-3 -3,-3 -1.05,0 -1.96,0.54 -2.5,1.35l-0.5,0.67 -0.5,-0.68C10.96,2.54 10.05,2 9,2 7.34,2 6,3.34 6,5c0,0.35 0.07,0.69 0.18,1L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM15,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM9,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM20,19L4,19v-2h16v2zM20,14L4,14L4,8h5.08L7,10.83 8.62,12 11,8.76l1,-1.36 1,1.36L15.38,12 17,10.83 14.92,8L20,8v6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_cast_connected_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM19,7L5,7v1.63c3.96,1.28 7.09,4.41 8.37,8.37L19,17L19,7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11zM21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_center_focus_weak_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M5,15L3,15v4c0,1.1 0.9,2 2,2h4v-2L5,19v-4zM5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2v4h2L5,5zM19,3h-4v2h4v4h2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19h-4v2h4c1.1,0 2,-0.9 2,-2v-4h-2v4zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_color_lens_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_compare_arrows_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M9.01,14H2v2h7.01v3L13,15l-3.99,-4V14zM14.99,13v-3H22V8h-7.01V5L11,9L14.99,13z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_domain_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,7V3H2v18h20V7H12zM6,19H4v-2h2V19zM6,15H4v-2h2V15zM6,11H4V9h2V11zM6,7H4V5h2V7zM10,19H8v-2h2V19zM10,15H8v-2h2V15zM10,11H8V9h2V11zM10,7H8V5h2V7zM20,19h-8v-2h2v-2h-2v-2h2v-2h-2V9h8V19zM18,11h-2v2h2V11zM18,15h-2v2h2V15z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_download_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_emoji_emotions_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12c0,5.52 4.47,10 9.99,10C17.52,22 22,17.52 22,12C22,6.48 17.52,2 11.99,2zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,18c-2.28,0 -4.22,-1.66 -5,-4h10C16.22,16.34 14.28,18 12,18zM15.5,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S14.67,8 15.5,8S17,8.67 17,9.5S16.33,11 15.5,11z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_fast_forward_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_fingerprint_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M9,12c0,1.66 1.34,3 3,3s3,-1.34 3,-3s-1.34,-3 -3,-3S9,10.34 9,12z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M8,10V8H5.09C6.47,5.61 9.05,4 12,4c3.72,0 6.85,2.56 7.74,6h2.06c-0.93,-4.56 -4.96,-8 -9.8,-8C8.73,2 5.82,3.58 4,6.01V4H2v6H8z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M16,14v2h2.91c-1.38,2.39 -3.96,4 -6.91,4c-3.72,0 -6.85,-2.56 -7.74,-6H2.2c0.93,4.56 4.96,8 9.8,8c3.27,0 6.18,-1.58 8,-4.01V20h2v-6H16z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_format_align_left_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_grid_3x3_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,10V8h-4V4h-2v4h-4V4H8v4H4v2h4v4H4v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4H20zM14,14h-4v-4h4V14z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_home_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_http_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4.5,11h-2L2.5,9L1,9v6h1.5v-2.5h2L4.5,15L6,15L6,9L4.5,9v2zM7,10.5h1.5L8.5,15L10,15v-4.5h1.5L11.5,9L7,9v1.5zM12.5,10.5L14,10.5L14,15h1.5v-4.5L17,10.5L17,9h-4.5v1.5zM21.5,9L18,9v6h1.5v-2h2c0.8,0 1.5,-0.7 1.5,-1.5v-1c0,-0.8 -0.7,-1.5 -1.5,-1.5zM21.5,11.5h-2v-1h2v1z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_https_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_import_contacts_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M17.5,4.5c-1.95,0 -4.05,0.4 -5.5,1.5c-1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6v14.65c0,0.65 0.73,0.45 0.75,0.45C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5c1.35,-0.85 3.8,-1.5 5.5,-1.5c1.65,0 3.35,0.3 4.75,1.05C22.66,21.26 23,20.86 23,20.6V6C21.51,4.88 19.37,4.5 17.5,4.5zM21,18.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5c-1.7,0 -4.15,0.65 -5.5,1.5V8c1.35,-0.85 3.8,-1.5 5.5,-1.5c1.2,0 2.4,0.15 3.5,0.5V18.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_info_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_layers_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_legend_toggle_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,15H4v-2h16V15zM20,17H4v2h16V17zM15,11l5,-3.55L20,5l-5,3.55L10,5L4,8.66L4,11l5.92,-3.61L15,11z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_link_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_local_bar_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M21,5V3H3v2l8,9v5H6v2h12v-2h-5v-5l8,-9zM7.43,7L5.66,5h12.69l-1.78,2H7.43z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_lock_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_low_priority_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M14,5h8v2h-8zM14,10.5h8v2h-8zM14,16h8v2h-8zM2,11.5C2,15.08 4.92,18 8.5,18L9,18v2l3,-3 -3,-3v2h-0.5C6.02,16 4,13.98 4,11.5S6.02,7 8.5,7L12,7L12,5L8.5,5C4.92,5 2,7.92 2,11.5z\"/>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_manage_search_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M7,9H2V7h5V9zM7,12H2v2h5V12zM20.59,19l-3.83,-3.83C15.96,15.69 15.02,16 14,16c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5s5,2.24 5,5c0,1.02 -0.31,1.96 -0.83,2.75L22,17.59L20.59,19zM17,11c0,-1.65 -1.35,-3 -3,-3s-3,1.35 -3,3s1.35,3 3,3S17,12.65 17,11zM2,19h10v-2H2V19z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_more_vert_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_multiline_chart_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M22,6.92l-1.41,-1.41 -2.85,3.21C15.68,6.4 12.83,5 9.61,5 6.72,5 4.07,6.16 2,8l1.42,1.42C5.12,7.93 7.27,7 9.61,7c2.74,0 5.09,1.26 6.77,3.24l-2.88,3.24 -4,-4L2,16.99l1.5,1.5 6,-6.01 4,4 4.05,-4.55c0.75,1.35 1.25,2.9 1.44,4.55H21c-0.22,-2.3 -0.95,-4.39 -2.04,-6.14L22,6.92z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_multiple_stop_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M17,4l4,4l-4,4V9h-4V7h4V4zM10,7C9.45,7 9,7.45 9,8s0.45,1 1,1s1,-0.45 1,-1S10.55,7 10,7zM6,7C5.45,7 5,7.45 5,8s0.45,1 1,1s1,-0.45 1,-1S6.55,7 6,7zM7,17h4v-2H7v-3l-4,4l4,4V17zM14,17c0.55,0 1,-0.45 1,-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1C13,16.55 13.45,17 14,17zM18,17c0.55,0 1,-0.45 1,-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1C17,16.55 17.45,17 18,17z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_nat_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M6.82,13H11v-2H6.82C6.4,9.84 5.3,9 4,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3C5.3,15 6.4,14.16 6.82,13zM4,13c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C5,12.55 4.55,13 4,13z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M23,12l-4,-3v2h-4.05C14.45,5.95 10.19,2 5,2v2c4.42,0 8,3.58 8,8s-3.58,8 -8,8v2c5.19,0 9.45,-3.95 9.95,-9H19v2L23,12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_nfc_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_no_encryption_gmailerrorred_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <sekai@neko.services>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2h-4.27L20,17.17V10c0,-1.1 -0.9,-2 -2,-2h-1V6c0,-2.76 -2.24,-5 -5,-5C9.79,1 7.93,2.45 7.27,4.44L8.9,6.07V6zM2.1,2.1L0.69,3.51L5.3,8.13C4.55,8.42 4,9.15 4,10v10c0,1.1 0.9,2 2,2h12c0.34,0 0.65,-0.09 0.93,-0.24l1.56,1.56l1.41,-1.41L2.1,2.1zM12,17c-1.1,0 -2,-0.9 -2,-2c0,-0.59 0.27,-1.12 0.68,-1.49l2.81,2.81C13.12,16.73 12.59,17 12,17z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_person_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_push_pin_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z\"\n      android:fillType=\"evenOdd\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_rule_folder_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,6h-8l-2,-2H4C2.9,4 2.01,4.9 2.01,6L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8C22,6.9 21.1,6 20,6zM7.83,16L5,13.17l1.41,-1.41l1.41,1.41l3.54,-3.54l1.41,1.41L7.83,16zM17.41,13L19,14.59L17.59,16L16,14.41L14.41,16L13,14.59L14.59,13L13,11.41L14.41,10L16,11.59L17.59,10L19,11.41L17.41,13z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_running_with_errors_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M22,10v8h-2v-8H22zM20,20v2h2v-2H20zM18,17.29C16.53,18.95 14.39,20 12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8v9l7.55,-7.55C17.72,3.34 15.02,2 12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c2.25,0 4.33,-0.74 6,-2V17.29z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_sanitizer_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M15.5,6.5C15.5,5.66 17,4 17,4s1.5,1.66 1.5,2.5C18.5,7.33 17.83,8 17,8S15.5,7.33 15.5,6.5zM19.5,15c1.38,0 2.5,-1.12 2.5,-2.5c0,-1.67 -2.5,-4.5 -2.5,-4.5S17,10.83 17,12.5C17,13.88 18.12,15 19.5,15zM13,14h-2v-2H9v2H7v2h2v2h2v-2h2V14zM16,12v10H4V12c0,-2.97 2.16,-5.43 5,-5.91V4H7V2h6c1.13,0 2.15,0.39 2.99,1.01l-1.43,1.43C14.1,4.17 13.57,4 13,4h-2v2.09C13.84,6.57 16,9.03 16,12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_security_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_shutter_speed_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M15,1L9,1v2h6L15,1zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7zM11.68,15L6.35,15c0.57,1.62 1.82,2.92 3.41,3.56l-0.11,-0.06 2.03,-3.5zM17.65,11c-0.57,-1.6 -1.78,-2.89 -3.34,-3.54L12.26,11h5.39zM10.61,18.83c0.45,0.11 0.91,0.17 1.39,0.17 1.34,0 2.57,-0.45 3.57,-1.19l-2.11,-3.9 -2.85,4.92zM7.55,8.99C6.59,10.05 6,11.46 6,13c0,0.34 0.04,0.67 0.09,1h4.72L7.55,8.99zM16.34,17.13C17.37,16.06 18,14.6 18,13c0,-0.34 -0.04,-0.67 -0.09,-1h-4.34l2.77,5.13zM13.33,7.15C12.9,7.06 12.46,7 12,7c-1.4,0 -2.69,0.49 -3.71,1.29l2.32,3.56 2.72,-4.7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_speed_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_stream_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M20,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4,12m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,20m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M10.05,8.59L6.03,4.55h-0.01l-0.31,-0.32 -1.42,1.41 4.02,4.05 0.01,-0.01 0.31,0.32zM13.943,8.617l4.405,-4.392L19.76,5.64l-4.405,4.393zM10.01,15.36l-1.42,-1.41 -4.03,4.01 -0.32,0.33 1.41,1.41 4.03,-4.02zM19.76,18.3l-3.99,-4.01 -0.36,-0.35L14,15.35l3.99,4.01 0.35,0.35z\"/>\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,4m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_texture_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19.51,3.08L3.08,19.51c0.09,0.34 0.27,0.65 0.51,0.9 0.25,0.24 0.56,0.42 0.9,0.51L20.93,4.49c-0.19,-0.69 -0.73,-1.23 -1.42,-1.41zM11.88,3L3,11.88v2.83L14.71,3h-2.83zM5,3c-1.1,0 -2,0.9 -2,2v2l4,-4L5,3zM19,21c0.55,0 1.05,-0.22 1.41,-0.59 0.37,-0.36 0.59,-0.86 0.59,-1.41v-2l-4,4h2zM9.29,21h2.83L21,12.12L21,9.29L9.29,21z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_timelapse_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M16.24,7.76C15.07,6.59 13.54,6 12,6v6l-4.24,4.24c2.34,2.34 6.14,2.34 8.49,0 2.34,-2.34 2.34,-6.14 -0.01,-8.48zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_transform_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M22,18v-2H8V4h2L7,1 4,4h2v2H2v2h4v8c0,1.1 0.9,2 2,2h8v2h-2l3,3 3,-3h-2v-2h4zM10,8h6v6h2V8c0,-1.1 -0.9,-2 -2,-2h-6v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_transgender_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,8c1.93,0 3.5,1.57 3.5,3.5S13.93,15 12,15s-3.5,-1.57 -3.5,-3.5S10.07,8 12,8zM16.53,8.38l3.97,-3.96V7h2V1h-6v2h2.58l-3.97,3.97C14.23,6.36 13.16,6 12,6c-1.16,0 -2.23,0.36 -3.11,0.97L8.24,6.32l1.41,-1.41L8.24,3.49L6.82,4.9L4.92,3H7.5V1h-6v6h2V4.42l1.91,1.9L3.99,7.74l1.41,1.41l1.41,-1.41l0.65,0.65C6.86,9.27 6.5,10.34 6.5,11.5c0,2.7 1.94,4.94 4.5,5.41L11,19H9v2h2v2h2v-2h2v-2h-2l0,-2.09c2.56,-0.47 4.5,-2.71 4.5,-5.41C17.5,10.34 17.14,9.27 16.53,8.38z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_update_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_view_list_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:autoMirrored=\"true\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M4,14h4v-4L4,10v4zM4,19h4v-4L4,15v4zM4,9h4L8,5L4,5v4zM9,14h12v-4L9,10v4zM9,19h12v-4L9,15v4zM9,5v4h12L21,5L9,5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_vpn_key_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_warning_24.xml",
    "content": "<!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_wb_sunny_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M6.76,4.84l-1.8,-1.79 -1.41,1.41 1.79,1.79 1.42,-1.41zM4,10.5L1,10.5v2h3v-2zM13,0.55h-2L11,3.5h2L13,0.55zM20.45,4.46l-1.41,-1.41 -1.79,1.79 1.41,1.41 1.79,-1.79zM17.24,18.16l1.79,1.8 1.41,-1.41 -1.8,-1.79 -1.4,1.4zM20,10.5v2h3v-2h-3zM12,5.5c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM11,22.45h2L13,19.5h-2v2.95zM3.55,18.54l1.41,1.41 1.79,-1.8 -1.41,-1.41 -1.79,1.8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_communication_phonelink_ring.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M20.1,7.7l-1,1c1.8,1.8 1.8,4.6 0,6.5l1,1c2.5,-2.3 2.5,-6.1 0,-8.5zM18,9.8l-1,1c0.5,0.7 0.5,1.6 0,2.3l1,1c1.2,-1.2 1.2,-3 0,-4.3zM14,1L4,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L16,3c0,-1.1 -0.9,-2 -2,-2zM14,20L4,20L4,4h10v16z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_device_data_usage.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M13,2.05v3.03c3.39,0.49 6,3.39 6,6.92 0,0.9 -0.18,1.75 -0.48,2.54l2.6,1.53c0.56,-1.24 0.88,-2.62 0.88,-4.07 0,-5.18 -3.95,-9.45 -9,-9.95zM12,19c-3.87,0 -7,-3.13 -7,-7 0,-3.53 2.61,-6.43 6,-6.92V2.05c-5.06,0.5 -9,4.76 -9,9.95 0,5.52 4.47,10 9.99,10 3.31,0 6.24,-1.61 8.06,-4.09l-2.6,-1.53C16.17,17.98 14.21,19 12,19z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_device_developer_mode.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M7,5h10v2h2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99L7,1c-1.1,0 -2,0.9 -2,2v4h2L7,5zM15.41,16.59L20,12l-4.59,-4.59L14,8.83 17.17,12 14,15.17l1.41,1.42zM10,15.17L6.83,12 10,8.83 8.59,7.41 4,12l4.59,4.59L10,15.17zM17,19L7,19v-2L5,17v4c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-4h-2v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_file_cloud_queue.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h0.71C7.37,7.69 9.48,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3s-1.34,3 -3,3z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_file_file_upload.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_hardware_router.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M20.2,5.9l0.8,-0.8C19.6,3.7 17.8,3 16,3s-3.6,0.7 -5,2.1l0.8,0.8C13,4.8 14.5,4.2 16,4.2s3,0.6 4.2,1.7zM19.3,6.7c-0.9,-0.9 -2.1,-1.4 -3.3,-1.4s-2.4,0.5 -3.3,1.4l0.8,0.8c0.7,-0.7 1.6,-1 2.5,-1 0.9,0 1.8,0.3 2.5,1l0.8,-0.8zM19,13h-2L17,9h-2v4L5,13c-1.1,0 -2,0.9 -2,2v4c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-4c0,-1.1 -0.9,-2 -2,-2zM8,18L6,18v-2h2v2zM11.5,18h-2v-2h2v2zM15,18h-2v-2h2v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_image_camera_alt.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0\"/>\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_image_edit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_image_looks_6.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M11,15h2v-2h-2v2zM19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM15,9h-4v2h2c1.1,0 2,0.89 2,2v2c0,1.11 -0.9,2 -2,2h-2c-1.1,0 -2,-0.89 -2,-2L9,9c0,-1.11 0.9,-2 2,-2h4v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_image_photo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z\"/>\n</vector>\n"
  },
  {
    "path": "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    android:tint=\"#000000\">\n  <group android:scaleX=\"0.286175\"\n      android:scaleY=\"0.286175\"\n      android:translateX=\"27.977152\"\n      android:translateY=\"25.15355\">\n    <group android:translateY=\"151.20006\">\n      <path android:pathData=\"M64.796875,-31L25.484375,-31L15.703125,0.5L4.609375,0.5L37.578125,-101L52.421875,-101L86.40625,0.5L75.171875,0.5L64.796875,-31ZM28.21875,-39.96875L61.921875,-39.96875L44.921875,-93.046875L28.21875,-39.96875Z\"\n          android:fillColor=\"#000000\"/>\n      <path android:pathData=\"M177.10938,0L164.875,0L135.64062,-46.6875L106.265625,0L94.171875,0L128.73438,-54.234375L97.625,-101.5L109.859375,-101.5L135.78125,-61.921875L161.98438,-101.5L173.79688,-101.5L142.40625,-54.8125L177.10938,0Z\"\n          android:fillColor=\"#000000\"/>\n    </group>\n  </group>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_maps_360.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:autoMirrored=\"true\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M12,7C6.48,7 2,9.24 2,12c0,2.24 2.94,4.13 7,4.77V20l4,-4 -4,-4v2.73c-3.15,-0.56 -5,-1.9 -5,-2.73 0,-1.06 3.04,-3 8,-3s8,1.94 8,3c0,0.73 -1.46,1.89 -4,2.53v2.05c3.53,-0.77 6,-2.53 6,-4.58 0,-2.76 -4.48,-5 -10,-5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_maps_directions.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M21.71,11.29l-9,-9c-0.39,-0.39 -1.02,-0.39 -1.41,0l-9,9c-0.39,0.39 -0.39,1.02 0,1.41l9,9c0.39,0.39 1.02,0.39 1.41,0l9,-9c0.39,-0.38 0.39,-1.01 0,-1.41zM14,14.5V12h-4v3H8v-4c0,-0.55 0.45,-1 1,-1h5V7.5l3.5,3.5 -3.5,3.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_maps_directions_boat.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M20,21c-1.39,0 -2.78,-0.47 -4,-1.32 -2.44,1.71 -5.56,1.71 -8,0C6.78,20.53 5.39,21 4,21H2v2h2c1.38,0 2.74,-0.35 4,-0.99 2.52,1.29 5.48,1.29 8,0 1.26,0.65 2.62,0.99 4,0.99h2v-2h-2zM3.95,19H4c1.6,0 3.02,-0.88 4,-2 0.98,1.12 2.4,2 4,2s3.02,-0.88 4,-2c0.98,1.12 2.4,2 4,2h0.05l1.89,-6.68c0.08,-0.26 0.06,-0.54 -0.06,-0.78s-0.34,-0.42 -0.6,-0.5L20,10.62V6c0,-1.1 -0.9,-2 -2,-2h-3V1H9v3H6c-1.1,0 -2,0.9 -2,2v4.62l-1.29,0.42c-0.26,0.08 -0.48,0.26 -0.6,0.5s-0.15,0.52 -0.06,0.78L3.95,19zM6,6h12v3.97L12,8 6,9.97V6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_navigation_apps.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_navigation_close.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_navigation_menu.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_notification_enhanced_encryption.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2H8.9V6zM16,16h-3v3h-2v-3H8v-2h3v-3h2v3h3v2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_qu_camera_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item\n        android:drawable=\"@drawable/ic_app_shortcut_background\"\n        android:left=\"2dp\"\n        android:top=\"2dp\"\n        android:right=\"2dp\"\n        android:bottom=\"2dp\" />\n\n    <item\n        android:drawable=\"@drawable/ic_image_camera_alt\"\n        android:left=\"12dp\"\n        android:top=\"12dp\"\n        android:right=\"12dp\"\n        android:bottom=\"12dp\" />\n\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/ic_qu_shadowsocks_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:name=\"path\"\n        android:fillColor=\"#000000\"\n        android:pathData=\"M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_qu_shadowsocks_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item\n        android:drawable=\"@drawable/ic_app_shortcut_background\"\n        android:left=\"2dp\"\n        android:top=\"2dp\"\n        android:right=\"2dp\"\n        android:bottom=\"2dp\" />\n\n    <item\n        android:drawable=\"@drawable/ic_qu_shadowsocks_foreground\"\n        android:left=\"12dp\"\n        android:top=\"12dp\"\n        android:right=\"12dp\"\n        android:bottom=\"12dp\" />\n\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_active.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:name=\"path\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_ax.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright (C) 2021 by nekohasekai <contact-git@sekai.icu>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\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    android:tint=\"#000000\">\n    <group android:scaleX=\"0.57\"\n        android:scaleY=\"0.55\"\n        android:translateX=\"0\"\n        android:translateY=\"0\">\n        <group android:translateY=\"151.20006\">\n            <path android:pathData=\"M64.796875,-31L25.484375,-31L15.703125,0.5L4.609375,0.5L37.578125,-101L52.421875,-101L86.40625,0.5L75.171875,0.5L64.796875,-31ZM28.21875,-39.96875L61.921875,-39.96875L44.921875,-93.046875L28.21875,-39.96875Z\"\n                android:fillColor=\"#000000\"/>\n            <path android:pathData=\"M177.10938,0L164.875,0L135.64062,-46.6875L106.265625,0L94.171875,0L128.73438,-54.234375L97.625,-101.5L109.859375,-101.5L135.78125,-61.921875L161.98438,-101.5L173.79688,-101.5L142.40625,-54.8125L177.10938,0Z\"\n                android:fillColor=\"#000000\"/>\n        </group>\n    </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_busy.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\">\n\n    <path\n        android:name=\"path\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_connected.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:drawable=\"@drawable/ic_service_busy\">\n    <target android:name=\"path\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"pathData\"\n                android:duration=\"@android:integer/config_mediumAnimTime\"\n                android:valueFrom=\"M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z\"\n                android:valueTo=\"M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z\"\n                android:valueType=\"pathType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_connecting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:drawable=\"@drawable/ic_service_idle\">\n    <target android:name=\"strike_thru_path\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"pathData\"\n                android:duration=\"@android:integer/config_mediumAnimTime\"\n                android:valueFrom=\"M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z\"\n                android:valueTo=\"M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z\"\n                android:valueType=\"pathType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n    <target android:name=\"strike_thru_mask\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"pathData\"\n                android:duration=\"@android:integer/config_mediumAnimTime\"\n                android:valueFrom=\"M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z\"\n                android:valueTo=\"M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z\"\n                android:valueType=\"pathType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_idle.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\">\n    <path\n        android:name=\"strike_thru_path\"\n        android:pathData=\"M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z\"\n        android:fillColor=\"#fff\"\n        android:strokeWidth=\"1\"/>\n    <clip-path\n        android:name=\"strike_thru_mask\"\n        android:pathData=\"M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z\"/>\n    <path\n        android:name=\"holey_icon\"\n        android:pathData=\"M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z\"\n        android:fillColor=\"#fff\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_stopped.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:drawable=\"@drawable/ic_service_idle\">\n    <target android:name=\"strike_thru_path\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"pathData\"\n                android:duration=\"@android:integer/config_mediumAnimTime\"\n                android:valueFrom=\"M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z\"\n                android:valueTo=\"M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z\"\n                android:valueType=\"pathType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n    <target android:name=\"strike_thru_mask\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"pathData\"\n                android:duration=\"@android:integer/config_mediumAnimTime\"\n                android:valueFrom=\"M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z\"\n                android:valueTo=\"M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z\"\n                android:valueType=\"pathType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_service_stopping.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:drawable=\"@drawable/ic_service_busy\">\n    <target android:name=\"path\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:propertyName=\"pathData\"\n                android:duration=\"@android:integer/config_mediumAnimTime\"\n                android:valueFrom=\"M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z\"\n                android:valueTo=\"M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z\"\n                android:valueType=\"pathType\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"/>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_settings_password.xml",
    "content": "<!-- https://android.googlesource.com/platform/packages/apps/Settings/+/7961482/res/drawable/ic_password.xml -->\n<!--\n    Copyright (C) 2017 The Android Open Source Project\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         http://www.apache.org/licenses/LICENSE-2.0\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"?attr/colorControlNormal\"\n        android:pathData=\"M21.5,9.39l-1.63,0l0.81,-1.42l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0z\" />\n    <path\n        android:fillColor=\"?attr/colorControlNormal\"\n        android:pathData=\"M13.68,7.97l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0l0,-1l-1.63,0z\" />\n    <path\n        android:fillColor=\"?attr/colorControlNormal\"\n        android:pathData=\"M6.68,7.97l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0l0,-1l-1.63,0z\" />\n    <path\n        android:fillColor=\"?attr/colorControlNormal\"\n        android:pathData=\"M21,17.89H3c-0.28,0 -0.5,-0.22 -0.5,-0.5v-1c0,-0.28 0.22,-0.5 0.5,-0.5h18c0.28,0 0.5,0.22 0.5,0.5v1C21.5,17.66 21.28,17.89 21,17.89z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_social_emoji_symbols.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M3,2h8v2h-8z\"/>\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M6,11l2,0l0,-4l3,0l0,-2l-8,0l0,2l3,0z\"/>\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M12.4036,20.1819l7.7781,-7.7781l1.4142,1.4142l-7.7781,7.7781z\"/>\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M14.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0\"/>\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19.5,19.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0\"/>\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M15.5,11c1.38,0 2.5,-1.12 2.5,-2.5V4h3V2h-4v4.51C16.58,6.19 16.07,6 15.5,6C14.12,6 13,7.12 13,8.5C13,9.88 14.12,11 15.5,11z\"/>\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M9.74,15.96l-1.41,1.41l-0.71,-0.71l0.35,-0.35c0.98,-0.98 0.98,-2.56 0,-3.54c-0.49,-0.49 -1.13,-0.73 -1.77,-0.73c-0.64,0 -1.28,0.24 -1.77,0.73c-0.98,0.98 -0.98,2.56 0,3.54l0.35,0.35l-1.06,1.06c-0.98,0.98 -0.98,2.56 0,3.54C4.22,21.76 4.86,22 5.5,22s1.28,-0.24 1.77,-0.73l1.06,-1.06l1.41,1.41l1.41,-1.41l-1.41,-1.41l1.41,-1.41L9.74,15.96zM5.85,14.2c0.12,-0.12 0.26,-0.15 0.35,-0.15s0.23,0.03 0.35,0.15c0.19,0.2 0.19,0.51 0,0.71l-0.35,0.35L5.85,14.9C5.66,14.71 5.66,14.39 5.85,14.2zM5.85,19.85C5.73,19.97 5.59,20 5.5,20s-0.23,-0.03 -0.35,-0.15c-0.19,-0.19 -0.19,-0.51 0,-0.71l1.06,-1.06l0.71,0.71L5.85,19.85z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_social_share.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:autoMirrored=\"true\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\"\n        android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/terminal_scroll_shape.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n\n    <solid android:color=\"?colorPrimary\" />\n\n    <size android:width=\"4dp\" />\n\n    <!--\n        <gradient\n        android:angle=\"45\"\n        android:centerColor=\"#66ff5c33\"\n        android:endColor=\"#66FF3401\"\n        android:startColor=\"#66FF3401\" />\n    \n    <corners android:radius=\"8dp\" />\n\n    <padding\n        android:left=\"0.5dp\"\n        android:right=\"0.5dp\" />\n    -->\n\n</shape>"
  },
  {
    "path": "app/src/main/res/layout/layout_about.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n\n            <com.google.android.material.card.MaterialCardView\n                android:id=\"@+id/title_card\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"8dp\"\n                android:layout_marginTop=\"8dp\"\n                android:layout_marginRight=\"8dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                app:cardElevation=\"2dp\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"center\"\n                    android:orientation=\"vertical\"\n                    android:paddingTop=\"40dp\"\n                    android:paddingBottom=\"40dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:fontFamily=\"@font/bgothm\"\n                        android:gravity=\"center\"\n                        android:text=\"@string/app_name\"\n                        android:textColor=\"?android:textColorPrimary\"\n                        android:textSize=\"70dp\"\n                        tools:ignore=\"SpUsage\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:text=\"@string/app_desc\"\n                        android:textAppearance=\"?attr/textAppearanceBody2\"\n                        android:textColor=\"?android:textColorSecondary\" />\n\n                </LinearLayout>\n\n            </com.google.android.material.card.MaterialCardView>\n\n            <androidx.coordinatorlayout.widget.CoordinatorLayout\n                android:id=\"@+id/about_fragment_holder\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:fitsSystemWindows=\"true\" />\n\n            <com.google.android.material.card.MaterialCardView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"8dp\"\n                android:layout_marginRight=\"8dp\"\n                android:layout_marginBottom=\"8dp\"\n                app:cardElevation=\"2dp\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\"\n                    android:padding=\"16dp\">\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:text=\"@string/license\"\n                        android:textAppearance=\"?attr/textAppearanceSubtitle2\" />\n\n                    <TextView\n                        android:id=\"@+id/license\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginTop=\"16dp\"\n                        android:textAppearance=\"?attr/textAppearanceBody2\"\n                        android:textColor=\"?android:textColorSecondary\"\n                        android:textIsSelectable=\"true\"\n                        android:textSize=\"12sp\" />\n\n                </LinearLayout>\n\n            </com.google.android.material.card.MaterialCardView>\n\n        </LinearLayout>\n\n    </androidx.core.widget.NestedScrollView>\n\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_add_entity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"16dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?android:attr/textAppearanceSmall\"\n            android:textColor=\"?android:attr/textColorPrimary\"\n            android:textStyle=\"bold\"\n            android:text=\"@string/add_profile\"/>\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_app_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:duplicateParentState=\"false\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.CollapsingToolbarLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_scrollFlags=\"scroll|enterAlways\"\n            app:titleEnabled=\"false\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                android:background=\"?attr/colorPrimary\"\n                android:elevation=\"4dp\"\n                android:theme=\"?actionBarTheme\"\n                android:touchscreenBlocksFocus=\"false\"\n                app:layout_collapseMode=\"pin\"\n                app:popupTheme=\"@style/ThemeOverlay.AppCompat.DayNight\"\n                app:title=\"@string/app_name\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?colorOnPrimarySurface\"\n                android:orientation=\"vertical\"\n                android:paddingTop=\"56dp\">\n\n                <com.google.android.material.card.MaterialCardView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"4dp\"\n                    app:cardElevation=\"2dp\">\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:orientation=\"vertical\"\n                        android:padding=\"8dp\">\n\n                        <com.google.android.material.chip.Chip\n                            android:id=\"@+id/show_system_apps\"\n                            style=\"@style/Widget.MaterialComponents.Chip.Filter\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginBottom=\"4dp\"\n                            android:text=\"@string/show_system_apps\" />\n\n                        <com.google.android.material.textfield.TextInputLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:hint=\"@string/search_apps\">\n\n                            <com.google.android.material.textfield.TextInputEditText\n                                android:id=\"@+id/search\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\" />\n\n                        </com.google.android.material.textfield.TextInputLayout>\n\n\n                    </LinearLayout>\n\n                </com.google.android.material.card.MaterialCardView>\n\n            </LinearLayout>\n\n        </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:id=\"@+id/loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.progressindicator.CircularProgressIndicator\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:indeterminate=\"true\" />\n\n    </LinearLayout>\n\n    <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\n        android:id=\"@+id/list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipToPadding=\"false\"\n        android:scrollbarSize=\"0dp\"\n        android:visibility=\"gone\"\n        app:fastScrollAutoHide=\"true\"\n        app:fastScrollPopupBgColor=\"?colorOnPrimarySurface\"\n        app:fastScrollPopupTextColor=\"?colorPrimary\"\n        app:fastScrollThumbColor=\"?colorPrimary\"\n        app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n        app:fastScrollTrackColor=\"?colorOnPrimarySurface\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        tools:listitem=\"@layout/layout_apps_item\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_appbar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.appbar.AppBarLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/appbar\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?attr/actionBarSize\"\n    android:elevation=\"4dp\">\n\n    <com.google.android.material.appbar.MaterialToolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"?attr/colorPrimary\"\n        android:theme=\"?actionBarTheme\"\n        android:touchscreenBlocksFocus=\"false\"\n        app:popupTheme=\"@style/ThemeOverlay.AppCompat.DayNight\"\n        app:title=\"@string/app_name\" />\n</com.google.android.material.appbar.AppBarLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_apps.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:duplicateParentState=\"false\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.google.android.material.appbar.CollapsingToolbarLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            app:layout_scrollFlags=\"scroll|enterAlways\"\n            app:titleEnabled=\"false\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                android:background=\"?attr/colorPrimary\"\n                android:elevation=\"4dp\"\n                android:theme=\"?actionBarTheme\"\n                android:touchscreenBlocksFocus=\"false\"\n                app:layout_collapseMode=\"pin\"\n                app:popupTheme=\"@style/ThemeOverlay.AppCompat.DayNight\"\n                app:title=\"@string/app_name\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?colorOnPrimarySurface\"\n                android:orientation=\"vertical\"\n                android:paddingTop=\"56dp\">\n\n                <com.google.android.material.card.MaterialCardView\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_margin=\"4dp\"\n                    app:cardElevation=\"2dp\">\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:orientation=\"vertical\"\n                        android:padding=\"8dp\">\n\n                        <com.google.android.material.chip.ChipGroup\n                            android:id=\"@+id/bypassGroup\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginTop=\"4dp\"\n                            app:selectionRequired=\"true\"\n                            app:singleLine=\"true\"\n                            app:singleSelection=\"true\">\n\n                            <com.google.android.material.chip.Chip\n                                android:id=\"@+id/appProxyModeDisable\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/off\" />\n\n                            <com.google.android.material.chip.Chip\n                                android:id=\"@+id/appProxyModeOn\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:text=\"@string/route_proxy\" />\n\n                            <com.google.android.material.chip.Chip\n                                android:id=\"@+id/appProxyModeBypass\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:checked=\"true\"\n                                android:text=\"@string/bypass_apps\" />\n\n                        </com.google.android.material.chip.ChipGroup>\n\n                        <com.google.android.material.chip.Chip\n                            android:id=\"@+id/show_system_apps\"\n                            style=\"@style/Widget.MaterialComponents.Chip.Filter\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_marginBottom=\"4dp\"\n                            android:text=\"@string/show_system_apps\" />\n\n                        <com.google.android.material.textfield.TextInputLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:hint=\"@string/search_apps\">\n\n                            <com.google.android.material.textfield.TextInputEditText\n                                android:id=\"@+id/search\"\n                                android:layout_width=\"match_parent\"\n                                android:layout_height=\"wrap_content\" />\n\n                        </com.google.android.material.textfield.TextInputLayout>\n\n\n                    </LinearLayout>\n\n                </com.google.android.material.card.MaterialCardView>\n\n            </LinearLayout>\n\n        </com.google.android.material.appbar.CollapsingToolbarLayout>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:id=\"@+id/loading\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.progressindicator.CircularProgressIndicator\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:indeterminate=\"true\" />\n\n    </LinearLayout>\n\n    <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\n        android:id=\"@+id/list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipToPadding=\"false\"\n        android:scrollbarSize=\"0dp\"\n        android:visibility=\"gone\"\n        app:fastScrollAutoHide=\"true\"\n        app:fastScrollPopupBgColor=\"?colorOnPrimarySurface\"\n        app:fastScrollPopupTextColor=\"?colorPrimary\"\n        app:fastScrollThumbColor=\"?colorPrimary\"\n        app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n        app:fastScrollTrackColor=\"?colorOnPrimarySurface\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        tools:listitem=\"@layout/layout_apps_item\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_apps_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    app:cardElevation=\"2dp\"\n    tools:ignore=\"SelectableText\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\"\n        android:padding=\"8dp\">\n\n        <ImageView\n            android:id=\"@+id/itemicon\"\n            android:layout_width=\"40dp\"\n            android:layout_height=\"40dp\"\n            android:layout_marginStart=\"8dp\"\n            android:importantForAccessibility=\"no\"\n            android:scaleType=\"fitCenter\"\n            tools:src=\"@android:drawable/sym_def_app_icon\" />\n\n        <LinearLayout\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"16dp\"\n            android:layout_marginTop=\"4dp\"\n            android:layout_marginEnd=\"8dp\"\n            android:layout_marginBottom=\"4dp\"\n            android:layout_weight=\"1\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:id=\"@+id/title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:ellipsize=\"end\"\n                android:textColor=\"?android:attr/textColorSecondary\"\n                android:textSize=\"18sp\"\n                tools:text=\"Package label\" />\n\n            <TextView\n                android:id=\"@+id/desc\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:breakStrategy=\"high_quality\"\n                android:ellipsize=\"end\"\n                android:textColor=\"?android:attr/textColorTertiary\"\n                android:textSize=\"12sp\"\n                tools:text=\"com.package.name (10000)\" />\n\n        </LinearLayout>\n\n        <androidx.appcompat.widget.SwitchCompat\n            android:id=\"@+id/itemcheck\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"16dp\"\n            android:clickable=\"false\"\n            android:focusable=\"false\"\n            android:focusableInTouchMode=\"false\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\" />\n\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_asset_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginLeft=\"8dp\"\n    android:layout_marginTop=\"8dp\"\n    android:layout_marginRight=\"8dp\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"bottom\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.progressindicator.LinearProgressIndicator\n            android:id=\"@+id/subscription_update_progress\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:indeterminate=\"true\"\n            android:visibility=\"invisible\"\n            app:indicatorColor=\"?selectedColorPrimary\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:baselineAligned=\"false\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center_vertical\"\n                android:paddingStart=\"15dp\"\n                android:paddingTop=\"15dp\"\n                android:paddingEnd=\"8dp\"\n                android:paddingBottom=\"12dp\">\n\n                <TextView\n                    android:id=\"@+id/asset_name\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceMedium\"\n                    android:textColor=\"?android:attr/textColorPrimary\"\n                    android:textStyle=\"bold\" />\n            </LinearLayout>\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"4dp\"\n            android:layout_marginRight=\"4dp\"\n            android:layout_marginBottom=\"4dp\"\n            android:gravity=\"center_vertical\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_marginBottom=\"8dp\"\n                android:layout_weight=\"1\"\n                android:gravity=\"bottom\"\n                android:paddingStart=\"12.5dp\">\n\n                <TextView\n                    android:id=\"@+id/asset_status\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\" />\n\n            </LinearLayout>\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/rules_update\"\n                style=\"?attr/borderlessButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"8dp\"\n                android:text=\"@string/group_update\"\n                android:textColor=\"?accentOrTextSecondary\" />\n        </LinearLayout>\n\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_assets.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/coordinator\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <include\n            layout=\"@layout/layout_appbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n            android:id=\"@+id/refresh_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/recycler_view\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:clipToPadding=\"false\" />\n\n        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n    </LinearLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_chain_settings.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <LinearLayout\n        android:id=\"@+id/list_cell\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:background=\"@android:color/darker_gray\"\n        android:orientation=\"horizontal\" />\n\n    <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\n        android:id=\"@+id/configuration_list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipToPadding=\"false\"\n        android:scrollbarSize=\"0dp\"\n        android:padding=\"4dp\"\n        android:overScrollMode=\"never\"\n        app:fastScrollAutoHide=\"true\"\n        app:fastScrollPopupBgColor=\"?colorOnPrimarySurface\"\n        app:fastScrollPopupTextColor=\"?colorPrimary\"\n        app:fastScrollThumbColor=\"?colorPrimary\"\n        app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n        app:fastScrollTrackColor=\"?colorOnPrimarySurface\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\">\n\n    </com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_cloudflare.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.card.MaterialCardView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"8dp\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        app:cardElevation=\"2dp\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\"\n                android:padding=\"16dp\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/cloudflare_wrap\"\n                    android:textAppearance=\"?attr/textAppearanceHeadline6\" />\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginTop=\"16dp\"\n                    android:autoLink=\"web\"\n                    android:text=\"@string/warp_license\"\n                    android:textAppearance=\"?attr/textAppearanceBody2\"\n                    android:textColor=\"?android:attr/textColorSecondary\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"8dp\"\n                android:gravity=\"end\"\n                android:orientation=\"horizontal\">\n\n                <Button\n                    android:id=\"@+id/warpGenerate\"\n                    style=\"@style/Widget.AppCompat.Button.Borderless\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginEnd=\"8dp\"\n                    android:text=\"@string/warp_generate\"\n                    android:textColor=\"?primaryOrTextPrimary\" />\n            </LinearLayout>\n\n        </LinearLayout>\n    </com.google.android.material.card.MaterialCardView>\n\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_config_settings.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_debug.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"16dp\">\n\n    <Button\n        android:id=\"@+id/debug_crash\"\n        style=\"@style/Widget.AppCompat.Button.Colored\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"Crash\"\n        android:textColor=\"?whiteOrTextPrimary\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_edit_config.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:background=\"@android:color/darker_gray\"></LinearLayout>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\">\n\n        <com.blacksquircle.ui.editorkit.widget.TextProcessor\n            android:id=\"@+id/editor\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@null\"\n            android:completionThreshold=\"2\"\n            android:importantForAutofill=\"no\"\n            android:popupBackground=\"@drawable/autocomplete_bg\"\n            android:scrollbars=\"none\"\n            android:textCursorDrawable=\"@drawable/caret\" />\n\n        <com.blacksquircle.ui.editorkit.widget.TextScroller\n            android:id=\"@+id/scroller\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"end\"\n            app:thumbTint=\"?colorPrimary\" />\n\n    </FrameLayout>\n\n    <LinearLayout\n        android:id=\"@+id/keyboard_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:elevation=\"2dp\"\n        android:orientation=\"horizontal\">\n\n        <ImageView\n            android:id=\"@+id/action_tab\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:background=\"?colorSurface\"\n            android:padding=\"8dp\"\n            android:src=\"@drawable/ic_keyboard_tab\"\n            app:tint=\"?colorOnSurface\" />\n\n        <com.blacksquircle.ui.feature.editor.customview.ExtendedKeyboard\n            android:id=\"@+id/extended_keyboard\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"?colorSecondaryVariant\"\n            android:orientation=\"horizontal\"\n            app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n            tools:itemCount=\"24\"\n            tools:listitem=\"@layout/item_keyboard_key\" />\n\n    </LinearLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_edit_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"12dp\">\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/group_name_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"4dp\"\n                android:layout_marginRight=\"4dp\"\n                android:layout_marginBottom=\"4dp\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/group_name\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"@string/group_name\"\n                    android:inputType=\"textNoSuggestions\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/group_links_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"4dp\"\n                android:layout_marginTop=\"8dp\"\n                android:layout_marginRight=\"4dp\"\n                android:layout_marginBottom=\"4dp\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    android:id=\"@+id/group_subscription_link\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:hint=\"@string/group_subscription_link\"\n                    android:inputType=\"textNoSuggestions|textMultiLine\" />\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.card.MaterialCardView\n                android:id=\"@+id/deduplication_card\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"4dp\"\n                android:layout_marginTop=\"8dp\"\n                android:layout_marginRight=\"4dp\"\n                android:layout_marginBottom=\"4dp\"\n                app:cardElevation=\"2dp\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"4dp\">\n\n                    <TextView\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_marginStart=\"12dp\"\n                        android:layout_weight=\"1\"\n                        android:text=\"@string/deduplication\"\n                        android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                        android:textStyle=\"bold\" />\n\n                    <com.google.android.material.checkbox.MaterialCheckBox\n                        android:id=\"@+id/deduplication\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\" />\n\n                </LinearLayout>\n\n            </com.google.android.material.card.MaterialCardView>\n\n        </LinearLayout>\n    </androidx.core.widget.NestedScrollView>\n\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_empty.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/fragment_holder\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\" />"
  },
  {
    "path": "app/src/main/res/layout/layout_empty_route.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"16dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:textAppearance=\"?android:attr/textAppearanceSmall\"\n            android:textColor=\"?android:attr/textColorPrimary\"\n            android:textStyle=\"bold\"\n            android:text=\"@string/route_warn\"/>\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <include layout=\"@layout/layout_appbar\" />\n\n        <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\n            android:id=\"@+id/group_list\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"4dp\"\n            android:scrollbarSize=\"0dp\"\n            app:fastScrollAutoHide=\"true\"\n            app:fastScrollThumbColor=\"?colorPrimary\"\n            app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n            app:fastScrollTrackColor=\"?colorOnPrimarySurface\">\n\n        </com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView>\n\n    </LinearLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_group_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"bottom\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.progressindicator.LinearProgressIndicator\n            android:id=\"@+id/subscription_update_progress\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:indeterminate=\"true\"\n            android:visibility=\"gone\"\n            app:indicatorColor=\"?selectedColorPrimary\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center_vertical\"\n                android:paddingStart=\"15dp\"\n                android:paddingTop=\"15dp\"\n                android:paddingEnd=\"8dp\"\n                android:paddingBottom=\"12dp\">\n\n                <TextView\n                    android:id=\"@+id/group_name\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginEnd=\"8dp\"\n                    android:textAppearance=\"?android:attr/textAppearanceMedium\"\n                    android:textColor=\"?android:attr/textColorPrimary\"\n                    android:textStyle=\"bold\"\n                    tools:text=\"@string/group_name\" />\n\n                <TextView\n                    android:id=\"@+id/group_user\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?android:attr/textColorSecondary\"\n                    tools:text=\"user name\" />\n            </LinearLayout>\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/edit\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:contentDescription=\"@string/edit\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\"\n                app:srcCompat=\"@drawable/ic_image_edit\" />\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/options\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:focusable=\"true\"\n                android:nextFocusLeft=\"@+id/container\"\n                android:padding=\"12dp\"\n                app:srcCompat=\"@drawable/ic_baseline_more_vert_24\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginLeft=\"4dp\"\n            android:layout_marginRight=\"4dp\"\n            android:layout_marginBottom=\"4dp\"\n            android:gravity=\"bottom\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"8dp\"\n                android:layout_weight=\"1\"\n                android:gravity=\"bottom\"\n                android:orientation=\"vertical\"\n                android:paddingStart=\"12.5dp\"\n                android:paddingTop=\"8dp\">\n\n                <TextView\n                    android:id=\"@+id/group_traffic\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\"\n                    android:visibility=\"gone\"\n                    tools:text=\"@string/subscription_traffic\" />\n\n                <TextView\n                    android:id=\"@+id/group_status\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\"\n                    tools:text=\"@string/group_status_empty\" />\n\n            </LinearLayout>\n\n            <com.google.android.material.button.MaterialButton\n                android:id=\"@+id/group_update\"\n                style=\"?attr/borderlessButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"8dp\"\n                android:text=\"@string/group_update\"\n                android:textColor=\"?accentOrTextSecondary\" />\n        </LinearLayout>\n\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_group_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.ConfigurationFragment\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/group_tab\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?colorPrimary\"\n        android:elevation=\"4dp\"\n        android:visibility=\"gone\"\n        app:tabIndicatorColor=\"?tabIndicatorColor\"\n        app:tabIndicatorFullWidth=\"false\"\n        app:tabMode=\"scrollable\"\n        app:tabPaddingStart=\"16dp\"\n        app:tabRippleColor=\"?tabRippleColor\"\n        app:tabSelectedTextColor=\"?tabSelectedTextColor\"\n        app:tabTextColor=\"?tabTextColor\"\n        app:tabUnboundedRipple=\"false\" />\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/group_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_icon_list_item_2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"wrap_content\"\n              android:background=\"?android:attr/selectableItemBackground\"\n              android:paddingStart=\"12dp\"\n              android:paddingEnd=\"12dp\"\n              android:focusable=\"true\">\n    <ImageView android:id=\"@android:id/icon\"\n               android:layout_width=\"52dp\"\n               android:layout_height=\"52dp\"\n               android:importantForAccessibility=\"no\"/>\n    <Space android:layout_width=\"4dp\"\n           android:layout_height=\"wrap_content\"/>\n    <LinearLayout android:orientation=\"vertical\"\n                  android:layout_width=\"0dp\"\n                  android:layout_height=\"wrap_content\"\n                  android:layout_weight=\"1\"\n                  android:layout_gravity=\"center_vertical\">\n        <TextView android:id=\"@android:id/text1\"\n                  android:textAppearance=\"?android:attr/textAppearance\"\n                  android:layout_width=\"match_parent\"\n                  android:layout_height=\"wrap_content\"\n                  android:maxLines=\"2\"\n                  android:ellipsize=\"end\"/>\n        <TextView android:id=\"@android:id/text2\"\n                  android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                  android:layout_width=\"match_parent\"\n                  android:layout_height=\"wrap_content\"\n                  android:maxLines=\"2\"\n                  android:ellipsize=\"end\"/>\n    </LinearLayout>\n    <ImageView\n        android:id=\"@+id/unlock\"\n        android:layout_width=\"36dp\"\n        android:layout_height=\"36dp\"\n        android:layout_gravity=\"center\"\n        android:paddingStart=\"12dp\"\n        android:src=\"@drawable/ic_action_lock_open\"\n        android:importantForAccessibility=\"no\"\n        tools:ignore=\"RtlSymmetry\"\n        app:tint=\"@color/material_amber_accent_700\" />\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_license.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/fragment_holder\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fitsSystemWindows=\"true\" />\n\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_link_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Based on: https://android.googlesource.com/platform/frameworks/support/+/b4cd329/preference/preference/res/layout/preference_dialog_edittext.xml -->\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:overScrollMode=\"ifContentScrolls\"\n    android:paddingTop=\"16dp\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@android:id/message\"\n            style=\"?android:attr/textAppearanceSmall\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"24dp\"\n            android:layout_marginLeft=\"24dp\"\n            android:layout_marginEnd=\"24dp\"\n            android:layout_marginRight=\"24dp\"\n            android:layout_marginBottom=\"48dp\"\n            android:textColor=\"?android:attr/textColorSecondary\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:id=\"@+id/input_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"20dp\"\n            android:layout_marginLeft=\"20dp\"\n            android:layout_marginEnd=\"20dp\"\n            android:layout_marginRight=\"20dp\"\n            android:orientation=\"vertical\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@android:id/edit\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:inputType=\"textUri\"\n                android:minHeight=\"48dp\"\n                android:paddingTop=\"12dp\"\n                android:singleLine=\"true\"\n                android:typeface=\"monospace\" />\n        </com.google.android.material.textfield.TextInputLayout>\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_loading.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.progressindicator.LinearProgressIndicator\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:indeterminate=\"true\"\n        app:indicatorColor=\"?selectedColorPrimary\" />\n\n    <androidx.core.widget.NestedScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_vertical\"\n        android:padding=\"16dp\">\n\n        <TextView\n            android:id=\"@+id/loadingText\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"Loading...\"\n            android:textAppearance=\"?textAppearanceSubtitle1\" />\n\n    </androidx.core.widget.NestedScrollView>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_logcat.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.ToolsFragment\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"8dp\">\n\n        <com.termux.view.TerminalView\n            android:id=\"@+id/terminal_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:autofillHints=\"password\"\n            android:focusableInTouchMode=\"true\"\n            android:scrollbarThumbVertical=\"@drawable/terminal_scroll_shape\"\n            android:scrollbars=\"vertical\" />\n    </LinearLayout>\n\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.drawerlayout.widget.DrawerLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/drawer_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    tools:openDrawer=\"start\">\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipChildren=\"false\"\n        android:fitsSystemWindows=\"true\">\n\n        <androidx.coordinatorlayout.widget.CoordinatorLayout\n            android:id=\"@+id/fragment_holder\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:fitsSystemWindows=\"true\" />\n\n        <!-- We double trackThickness as half of it will be invisible -->\n        <com.google.android.material.progressindicator.CircularProgressIndicator\n            android:id=\"@+id/fabProgress\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:max=\"1\"\n            android:visibility=\"invisible\"\n            app:indicatorColor=\"?colorMaterial100\"\n            app:layout_anchor=\"@+id/fab\"\n            app:layout_anchorGravity=\"center\"\n            app:layout_behavior=\"io.nekohasekai.sagernet.widget.FabProgressBehavior\"\n            app:trackCornerRadius=\"@dimen/mtrl_progress_track_thickness\"\n            app:trackThickness=\"8dp\" />\n\n        <io.nekohasekai.sagernet.widget.ServiceButton\n            android:id=\"@+id/fab\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:elevation=\"6dp\"\n            android:nextFocusDown=\"@+id/stats\"\n            app:backgroundTint=\"?fabColorBackground\"\n            app:layout_anchor=\"@id/stats\"\n            app:pressedTranslationZ=\"6dp\"\n            app:srcCompat=\"@drawable/ic_service_idle\" />\n\n        <io.nekohasekai.sagernet.widget.StatsBar\n            android:id=\"@+id/stats\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:fitsSystemWindows=\"true\"\n            android:nextFocusUp=\"@+id/fab\"\n            app:backgroundTint=\"?colorMaterial300\"\n            app:contentInsetStart=\"0dp\"\n            app:fabAlignmentMode=\"end\"\n            app:fabCradleMargin=\"0dp\"\n            app:fabCradleRoundedCornerRadius=\"0dp\"\n            app:fabCradleVerticalOffset=\"8dp\"\n            app:hideOnScroll=\"true\"\n            app:layout_scrollFlags=\"enterAlways|scroll\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?android:attr/selectableItemBackground\"\n                android:orientation=\"vertical\"\n                android:padding=\"16dp\">\n\n                <TextView\n                    android:id=\"@+id/tx\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_columnWeight=\"1\"\n                    android:layout_gravity=\"fill_horizontal\"\n                    android:ellipsize=\"marquee\"\n                    android:singleLine=\"true\"\n                    android:textColor=\"?whiteOrTextPrimary\"\n                    android:textSize=\"14sp\" />\n\n                <TextView\n                    android:id=\"@+id/rx\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_columnWeight=\"1\"\n                    android:layout_gravity=\"fill_horizontal\"\n                    android:ellipsize=\"marquee\"\n                    android:singleLine=\"true\"\n                    android:textColor=\"?whiteOrTextPrimary\"\n                    android:textSize=\"14sp\" />\n\n                <TextView\n                    android:id=\"@+id/status\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"fill_horizontal\"\n                    android:ellipsize=\"end\"\n                    android:maxLines=\"1\"\n                    android:textColor=\"?whiteOrTextPrimary\"\n                    android:textSize=\"16sp\"\n                    tools:text=\"@string/connection_test_available\" />\n\n            </LinearLayout>\n\n        </io.nekohasekai.sagernet.widget.StatsBar>\n\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n    <com.google.android.material.navigation.NavigationView\n        android:id=\"@+id/nav_view\"\n        style=\"@style/Widget.Design.NavigationView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"start\"\n        android:fitsSystemWindows=\"true\"\n        app:itemIconTint=\"@color/navigation_icon\"\n        app:itemTextColor=\"@color/navigation_icon\"\n        app:menu=\"@menu/main_drawer_menu\" />\n\n    <com.google.android.material.navigation.NavigationView\n        android:id=\"@+id/nav_view_black\"\n        style=\"@style/Widget.Design.NavigationView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"start\"\n        android:fitsSystemWindows=\"true\"\n        app:itemIconTint=\"@color/navigation_icon\"\n        app:itemShapeAppearance=\"@style/ShapeAppearance.MaterialComponents.MediumComponent\"\n        app:itemShapeFillColor=\"@color/navigation_item\"\n        app:itemTextColor=\"@color/navigation_icon\"\n        app:menu=\"@menu/main_drawer_menu\" />\n\n</androidx.drawerlayout.widget.DrawerLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_password_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Based on: https://android.googlesource.com/platform/frameworks/support/+/b4cd329/preference/preference/res/layout/preference_dialog_edittext.xml -->\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_marginTop=\"48dp\"\n    android:layout_marginBottom=\"48dp\"\n    android:overScrollMode=\"ifContentScrolls\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@android:id/message\"\n            style=\"?android:attr/textAppearanceSmall\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"24dp\"\n            android:layout_marginLeft=\"24dp\"\n            android:layout_marginEnd=\"24dp\"\n            android:layout_marginRight=\"24dp\"\n            android:layout_marginBottom=\"48dp\"\n            android:textColor=\"?android:attr/textColorSecondary\" />\n\n        <com.google.android.material.textfield.TextInputLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginStart=\"20dp\"\n            android:layout_marginLeft=\"20dp\"\n            android:layout_marginEnd=\"20dp\"\n            android:layout_marginRight=\"20dp\"\n            android:orientation=\"vertical\"\n            app:passwordToggleEnabled=\"true\">\n\n            <com.google.android.material.textfield.TextInputEditText\n                android:id=\"@android:id/edit\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:inputType=\"textPassword\"\n                android:minHeight=\"48dp\"\n                android:paddingTop=\"12dp\"\n                android:singleLine=\"true\"\n                android:typeface=\"monospace\" />\n        </com.google.android.material.textfield.TextInputLayout>\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_profile.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:id=\"@+id/content_lin\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:focusable=\"true\"\n        android:orientation=\"horizontal\">\n\n        <LinearLayout\n            android:id=\"@+id/selected_view\"\n            android:layout_width=\"4dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"?selectedColorPrimary\"\n            android:orientation=\"horizontal\"\n            android:visibility=\"invisible\"\n            tools:visibility=\"visible\" />\n\n        <LinearLayout\n\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:nextFocusRight=\"@+id/edit\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:id=\"@+id/container\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <LinearLayout\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:layout_weight=\"1\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingLeft=\"12dp\"\n                    android:paddingTop=\"8dp\"\n                    android:paddingBottom=\"8dp\">\n\n                    <TextView\n                        android:id=\"@+id/profile_name\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                        android:textColor=\"?android:attr/textColorPrimary\"\n                        android:textStyle=\"bold\"\n                        tools:text=\"@string/profile_name\" />\n                </LinearLayout>\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/edit\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"top\"\n                    android:background=\"?attr/selectableItemBackgroundBorderless\"\n                    android:contentDescription=\"@string/edit\"\n                    android:focusable=\"true\"\n                    android:padding=\"12dp\"\n                    app:srcCompat=\"@drawable/ic_image_edit\" />\n\n                <LinearLayout\n                    android:id=\"@+id/share\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"top\"\n                    android:background=\"?attr/selectableItemBackgroundBorderless\"\n                    android:contentDescription=\"@string/share\"\n                    android:focusable=\"true\"\n                    android:nextFocusLeft=\"@+id/container\">\n\n                    <LinearLayout\n                        android:id=\"@+id/share_layer\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:padding=\"12dp\">\n\n                        <androidx.appcompat.widget.AppCompatImageView\n                            android:id=\"@+id/shareIcon\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            app:srcCompat=\"@drawable/ic_social_share\" />\n                    </LinearLayout>\n\n                </LinearLayout>\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:paddingStart=\"12dp\"\n                android:paddingEnd=\"8dp\"\n                android:paddingBottom=\"4dp\"\n                android:visibility=\"gone\"\n                tools:visibility=\"visible\">\n\n                <TextView\n                    android:id=\"@+id/profile_address\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/server_address\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?android:attr/textColorSecondary\" />\n\n                <LinearLayout\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\" />\n\n                <TextView\n                    android:id=\"@+id/traffic_text\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/traffic\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?android:attr/textColorSecondary\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:paddingStart=\"12dp\"\n                android:paddingEnd=\"8dp\"\n                android:paddingBottom=\"12dp\">\n\n                <TextView\n                    android:id=\"@+id/profile_type\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\" />\n\n                <LinearLayout\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\" />\n\n                <TextView\n                    android:id=\"@+id/profile_status\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginStart=\"4dp\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\" />\n\n            </LinearLayout>\n\n        </LinearLayout>\n\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_profile_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/configuration_list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"4dp\"\n    android:scrollbarSize=\"0dp\"\n    app:fastScrollAutoHide=\"true\"\n    app:fastScrollThumbColor=\"?colorPrimary\"\n    app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n    app:fastScrollTrackColor=\"?colorOnPrimarySurface\"\n    tools:listitem=\"@layout/layout_profile\" />"
  },
  {
    "path": "app/src/main/res/layout/layout_progress.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\"\n    android:padding=\"16dp\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:indeterminate=\"true\"\n        app:indicatorColor=\"?selectedColorPrimary\" />\n\n    <TextView\n        android:id=\"@+id/content\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"16dp\"\n        android:text=\"@string/generating\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_progress_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.progressindicator.LinearProgressIndicator\n        android:id=\"@+id/progress_circular\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:indeterminate=\"true\"\n        app:indicatorColor=\"?selectedColorPrimary\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/list_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_route.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <include layout=\"@layout/layout_appbar\" />\n\n        <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\n            android:id=\"@+id/route_list\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"4dp\"\n            android:scrollbarSize=\"0dp\"\n            app:fastScrollAutoHide=\"true\"\n            app:fastScrollThumbColor=\"?colorPrimary\"\n            app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n            app:fastScrollTrackColor=\"?colorOnPrimarySurface\">\n\n        </com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView>\n\n    </LinearLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_route_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:id=\"@+id/content_lin\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:focusable=\"true\"\n        android:orientation=\"vertical\">\n\n\n        <LinearLayout\n            android:id=\"@+id/container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center_vertical\"\n                android:paddingStart=\"12dp\"\n                android:paddingTop=\"8dp\"\n                android:paddingBottom=\"8dp\">\n\n                <TextView\n                    android:id=\"@+id/profile_name\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?android:attr/textColorPrimary\"\n                    android:textStyle=\"bold\"\n                    tools:text=\"@string/profile_name\" />\n            </LinearLayout>\n\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/subscription\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:contentDescription=\"@string/subscriptions\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\"\n                android:visibility=\"gone\"\n                app:srcCompat=\"@drawable/ic_file_cloud_queue\" />\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/edit\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:contentDescription=\"@string/edit\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\"\n                app:srcCompat=\"@drawable/ic_image_edit\" />\n\n            <LinearLayout\n                android:id=\"@+id/share\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:contentDescription=\"@string/share\"\n                android:focusable=\"true\"\n                android:nextFocusLeft=\"@+id/container\"\n                android:visibility=\"gone\">\n\n                <LinearLayout\n                    android:id=\"@+id/share_layer\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:padding=\"12dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:id=\"@+id/shareIcon\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        app:srcCompat=\"@drawable/ic_social_share\" />\n                </LinearLayout>\n\n\n            </LinearLayout>\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"bottom\"\n            android:orientation=\"horizontal\"\n            android:paddingStart=\"12dp\"\n            android:paddingEnd=\"8dp\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:orientation=\"vertical\">\n\n                <TextView\n                    android:id=\"@+id/profile_type\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\" />\n\n                <TextView\n                    android:layout_marginTop=\"4dp\"\n                    android:layout_marginBottom=\"12dp\"\n                    android:id=\"@+id/route_outbound\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                    android:textColor=\"?attr/accentOrTextSecondary\" />\n\n            </LinearLayout>\n\n            <androidx.appcompat.widget.SwitchCompat\n                android:layout_marginBottom=\"2dp\"\n                android:id=\"@+id/enable\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n        </LinearLayout>\n\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_scanner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include layout=\"@layout/layout_appbar\" />\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/coordinator\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.journeyapps.barcodescanner.DecoratedBarcodeView\n            android:id=\"@+id/barcode_scanner\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:zxing_preview_scaling_strategy=\"centerCrop\"\n            app:zxing_use_texture_view=\"true\" />\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/layout_settings_activity.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/settings\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/configuration_list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_tools.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.ToolsFragment\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/tools_tab\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?colorPrimary\"\n        android:elevation=\"4dp\"\n        app:tabIndicatorColor=\"?tabIndicatorColor\"\n        app:tabIndicatorFullWidth=\"false\"\n        app:tabMode=\"fixed\"\n        app:tabPaddingStart=\"16dp\"\n        app:tabRippleColor=\"?tabRippleColor\"\n        app:tabSelectedTextColor=\"?tabSelectedTextColor\"\n        app:tabTextColor=\"?tabTextColor\"\n        app:tabUnboundedRipple=\"false\" />\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/tools_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_traffic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    tools:context=\".ui.TrafficFragment\">\n\n    <include\n        layout=\"@layout/layout_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/traffic_tab\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?colorPrimary\"\n        android:elevation=\"4dp\"\n        app:tabIndicatorColor=\"?tabIndicatorColor\"\n        app:tabIndicatorFullWidth=\"false\"\n        app:tabMode=\"fixed\"\n        app:tabPaddingStart=\"16dp\"\n        app:tabRippleColor=\"?tabRippleColor\"\n        app:tabSelectedTextColor=\"?tabSelectedTextColor\"\n        app:tabTextColor=\"?tabTextColor\"\n        app:tabUnboundedRipple=\"false\" />\n\n    <androidx.viewpager2.widget.ViewPager2\n        android:id=\"@+id/traffic_pager\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/layout_traffic_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    app:cardElevation=\"2dp\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"4dp\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:padding=\"12dp\">\n\n            <ImageView\n                android:id=\"@+id/icon\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:importantForAccessibility=\"no\"\n                android:scaleType=\"fitCenter\"\n                tools:src=\"@android:drawable/sym_def_app_icon\" />\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"16dp\"\n                android:layout_marginTop=\"4dp\"\n                android:layout_marginEnd=\"8dp\"\n                android:layout_marginBottom=\"4dp\"\n                android:layout_weight=\"1\"\n                android:orientation=\"vertical\">\n\n                <TextView\n                    android:id=\"@+id/label\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"?android:attr/textColorSecondary\"\n                    android:textSize=\"18sp\"\n                    tools:text=\"Package label\" />\n\n                <TextView\n                    android:id=\"@+id/desc\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:breakStrategy=\"high_quality\"\n                    android:ellipsize=\"end\"\n                    android:textColor=\"?android:attr/textColorTertiary\"\n                    android:textSize=\"12sp\"\n                    tools:text=\"com.package.name (10000)\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:id=\"@+id/menu\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:background=\"?attr/selectableItemBackgroundBorderless\"\n                android:focusable=\"true\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:padding=\"12dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:id=\"@+id/menuIcon\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        app:srcCompat=\"@drawable/ic_baseline_more_vert_24\" />\n                </LinearLayout>\n\n            </LinearLayout>\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:paddingStart=\"12dp\"\n            android:paddingEnd=\"8dp\"\n            android:paddingBottom=\"4dp\">\n\n            <TextView\n                android:id=\"@+id/tcp_connections\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/tcp_connections\"\n                android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                android:textColor=\"?android:attr/textColorSecondary\" />\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\" />\n\n            <TextView\n                android:id=\"@+id/traffic_uplink\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/traffic_uplink\"\n                android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                android:textColor=\"?android:attr/textColorSecondary\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:paddingStart=\"12dp\"\n            android:paddingEnd=\"8dp\"\n            android:paddingBottom=\"12dp\">\n\n            <TextView\n                android:id=\"@+id/udp_connections\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/udp_connections\"\n                android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                android:textColor=\"?android:attr/textColorSecondary\" />\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\" />\n\n            <TextView\n                android:id=\"@+id/traffic_downlink\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"4dp\"\n                android:text=\"@string/traffic_downlink\"\n                android:textAppearance=\"?android:attr/textAppearanceSmall\"\n                android:textColor=\"?android:attr/textColorSecondary\" />\n\n        </LinearLayout>\n\n    </LinearLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/layout_traffic_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\">\n\n    <TextView\n        android:id=\"@+id/holder\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/no_statistics\"\n        android:textAppearance=\"?android:attr/textAppearanceMedium\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n    <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView\n        android:id=\"@+id/traffic_list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"4dp\"\n        android:scrollbarSize=\"0dp\"\n        android:visibility=\"gone\"\n        app:fastScrollAutoHide=\"true\"\n        app:fastScrollThumbColor=\"?colorPrimary\"\n        app:fastScrollThumbInactiveColor=\"?colorPrimary\"\n        app:fastScrollTrackColor=\"?colorOnPrimarySurface\"\n        tools:listitem=\"@layout/layout_profile\" />\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/menu/add_group_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_new_group\"\n        android:icon=\"@drawable/ic_av_playlist_add\"\n        android:title=\"@string/group_create\"\n        app:showAsAction=\"always\" />\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/add_profile_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:alphabeticShortcut=\"n\"\n        android:icon=\"@drawable/ic_action_note_add\"\n        android:title=\"@string/add_profile\"\n        app:showAsAction=\"always\">\n        <menu>\n            <item\n                android:id=\"@+id/action_scan_qr_code\"\n                android:title=\"@string/add_profile_methods_scan_qr_code\" />\n            <item\n                android:id=\"@+id/action_import_clipboard\"\n                android:title=\"@string/action_import\" />\n            <item\n                android:id=\"@+id/action_import_file\"\n                android:title=\"@string/action_import_file\" />\n            <item android:title=\"@string/add_profile_methods_manual_settings\">\n                <menu>\n                    <item\n                        android:id=\"@+id/action_new_socks\"\n                        android:title=\"@string/action_socks\" />\n                    <item\n                        android:id=\"@+id/action_new_http\"\n                        android:title=\"@string/action_http\" />\n                    <item\n                        android:id=\"@+id/action_new_ss\"\n                        android:title=\"@string/action_shadowsocks\" />\n                    <item\n                        android:id=\"@+id/action_new_ssr\"\n                        android:title=\"@string/action_shadowsocksr\" />\n                    <item\n                        android:id=\"@+id/action_new_vmess\"\n                        android:title=\"@string/action_vmess\" />\n                    <item\n                        android:id=\"@+id/action_new_vless\"\n                        android:title=\"@string/action_vless\" />\n                    <item\n                        android:id=\"@+id/action_new_trojan\"\n                        android:title=\"@string/action_trojan\" />\n                    <item\n                        android:id=\"@+id/action_new_trojan_go\"\n                        android:title=\"@string/action_trojan_go\" />\n                    <item\n                        android:id=\"@+id/action_new_naive\"\n                        android:title=\"@string/action_naive\" />\n                    <item\n                        android:id=\"@+id/action_new_ping_tunnel\"\n                        android:title=\"@string/action_ping_tunnel\" />\n                    <item\n                        android:id=\"@+id/action_new_relay_baton\"\n                        android:title=\"@string/action_relay_baton\" />\n                    <item\n                        android:id=\"@+id/action_new_brook\"\n                        android:title=\"@string/action_brook\" />\n                    <item\n                        android:id=\"@+id/action_new_hysteria\"\n                        android:title=\"@string/action_hysteria\" />\n                    <item\n                        android:id=\"@+id/action_new_snell\"\n                        android:title=\"@string/action_snell\" />\n                    <item\n                        android:id=\"@+id/action_new_ssh\"\n                        android:title=\"@string/action_ssh\" />\n                    <item\n                        android:id=\"@+id/action_new_wg\"\n                        android:title=\"@string/action_wireguard\" />\n                    <item\n                        android:id=\"@+id/action_new_config\"\n                        android:title=\"@string/custom_config\" />\n                    <item\n                        android:id=\"@+id/action_new_chain\"\n                        android:title=\"@string/proxy_chain\" />\n                    <!--<item\n                        android:id=\"@+id/action_new_balancer\"\n                        android:title=\"@string/balancer\" />-->\n                </menu>\n            </item>\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_clear_traffic_statistics\"\n        android:title=\"@string/clear_traffic_statistics\" />\n    <item\n        android:id=\"@+id/action_connection_test\"\n        android:title=\"@string/connection_test\">\n        <menu>\n            <item\n                android:id=\"@+id/action_connection_icmp_ping\"\n                android:title=\"@string/connection_test_icmp_ping\" />\n            <item\n                android:id=\"@+id/action_connection_tcp_ping\"\n                android:title=\"@string/connection_test_tcp_ping\" />\n            <item\n                android:id=\"@+id/action_connection_url_test\"\n                android:title=\"@string/connection_test_url_test\" />\n            <item\n                android:id=\"@+id/action_connection_test_clear_results\"\n                android:title=\"@string/connection_test_clear_results\" />\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_order\"\n        android:title=\"@string/group_order\">\n        <menu>\n            <group android:checkableBehavior=\"single\">\n                <item\n                    android:id=\"@+id/action_order_origin\"\n                    android:title=\"@string/group_order_origin\" />\n                <item\n                    android:id=\"@+id/action_order_by_name\"\n                    android:title=\"@string/group_order_by_name\" />\n                <item\n                    android:id=\"@+id/action_order_by_delay\"\n                    android:title=\"@string/group_order_by_delay\" />\n            </group>\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_group_filter\"\n        android:title=\"@string/group_filter\">\n        <menu>\n            <item\n                android:id=\"@+id/action_filter_groups\"\n                android:title=\"@string/group_filter_groups\" />\n            <item\n                android:id=\"@+id/group_filter_owners\"\n                android:title=\"@string/group_filter_owners\" />\n            <item\n                android:id=\"@+id/action_filter_tags\"\n                android:title=\"@string/group_filter_tags\" />\n        </menu>\n    </item>\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/add_route_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_new_route\"\n        android:icon=\"@drawable/ic_baseline_add_road_24\"\n        android:title=\"@string/route_add\"\n        app:showAsAction=\"ifRoom\" />\n    <item\n        android:id=\"@+id/action_reset_route\"\n        android:title=\"@string/route_reset\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/action_manage_assets\"\n        android:title=\"@string/route_manage_assets\"\n        app:showAsAction=\"never\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/app_list_menu.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_invert_selections\"\n        android:title=\"@string/invert_selections\" />\n    <item\n        android:id=\"@+id/action_clear_selections\"\n        android:title=\"@string/clear_selections\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/action_export_clipboard\"\n        android:title=\"@string/action_export_clipboard\" />\n    <item\n        android:id=\"@+id/action_import_clipboard\"\n        android:title=\"@string/action_import\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/group_action_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:id=\"@+id/action_share\"\n        android:title=\"@string/share\">\n        <menu>\n            <item\n                android:id=\"@+id/action_universal_clipboard\"\n                android:title=\"@string/action_export_clipboard\" />\n            <item\n                android:id=\"@+id/action_universal_qr\"\n                android:title=\"@string/share_qr_nfc\" />\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_export\"\n        android:title=\"@string/action_export\">\n        <menu>\n            <item\n                android:id=\"@+id/action_export_clipboard\"\n                android:title=\"@string/action_export_clipboard\" />\n            <item\n                android:id=\"@+id/action_export_file\"\n                android:title=\"@string/action_export_file\" />\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_clear\"\n        android:title=\"@string/clear_profiles\" />\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/import_asset_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_import_file\"\n        android:icon=\"@drawable/ic_action_note_add\"\n        android:title=\"@string/action_import_file\"\n        app:showAsAction=\"always\" />\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/logcat_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_send_logcat\"\n        android:icon=\"@drawable/baseline_send_24\"\n        android:title=\"@string/logcat\"\n        app:showAsAction=\"ifRoom\" />\n    <item\n        android:id=\"@+id/action_clear_logcat\"\n        android:icon=\"@drawable/baseline_delete_sweep_24\"\n        android:title=\"@string/clear_logcat\"\n        app:showAsAction=\"always\" />\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/main_drawer_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <group android:checkableBehavior=\"single\">\n        <item\n            android:id=\"@+id/nav_configuration\"\n            android:icon=\"@drawable/ic_action_description\"\n            android:title=\"@string/menu_configuration\" />\n\n        <item\n            android:id=\"@+id/nav_group\"\n            android:icon=\"@drawable/ic_baseline_view_list_24\"\n            android:title=\"@string/menu_group\" />\n\n        <item\n            android:id=\"@+id/nav_route\"\n            android:icon=\"@drawable/ic_maps_directions\"\n            android:title=\"@string/menu_route\" />\n\n        <item\n            android:id=\"@+id/nav_settings\"\n            android:icon=\"@drawable/ic_action_settings\"\n            android:title=\"@string/settings\" />\n    </group>\n\n    <group\n        android:id=\"@+id/misc\"\n        android:checkableBehavior=\"single\">\n        <item\n            android:id=\"@+id/nav_logcat\"\n            android:icon=\"@drawable/ic_baseline_bug_report_24\"\n            android:title=\"@string/menu_log\" />\n        <item\n            android:id=\"@+id/nav_traffic\"\n            android:icon=\"@drawable/ic_baseline_transform_24\"\n            android:title=\"@string/menu_traffic\" />\n        <item\n            android:id=\"@+id/nav_tools\"\n            android:icon=\"@drawable/baseline_construction_24\"\n            android:title=\"@string/menu_tools\" />\n    </group>\n\n    <group\n        android:id=\"@+id/about\"\n        android:checkableBehavior=\"single\">\n        <item\n            android:id=\"@+id/nav_faq\"\n            android:icon=\"@drawable/ic_device_data_usage\"\n            android:title=\"@string/document\" />\n        <item\n            android:id=\"@+id/nav_about\"\n            android:icon=\"@drawable/ic_baseline_info_24\"\n            android:title=\"@string/menu_about\" />\n    </group>\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/per_app_proxy_menu.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_scan_china_apps\"\n        android:title=\"@string/action_scan_china_apps\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/action_invert_selections\"\n        android:title=\"@string/invert_selections\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/action_clear_selections\"\n        android:title=\"@string/clear_selections\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/action_export_clipboard\"\n        android:icon=\"?attr/actionModeCopyDrawable\"\n        android:title=\"@string/action_export_clipboard\"\n        app:showAsAction=\"ifRoom\" />\n    <item\n        android:id=\"@+id/action_import_clipboard\"\n        android:icon=\"?attr/actionModePasteDrawable\"\n        android:title=\"@string/action_import\"\n        app:showAsAction=\"always\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/profile_apply_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item android:id=\"@+id/action_apply\"\n          android:title=\"@string/apply\"\n          android:icon=\"@drawable/ic_action_done\"\n          android:alphabeticShortcut=\"s\"\n          app:showAsAction=\"always\"/>\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/profile_config_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item android:id=\"@+id/action_delete\"\n          android:title=\"@string/delete\"\n          android:icon=\"@drawable/ic_action_delete\"\n          android:alphabeticShortcut=\"d\"\n          app:showAsAction=\"ifRoom\"/>\n    <item android:id=\"@+id/action_apply\"\n          android:title=\"@string/apply\"\n          android:icon=\"@drawable/ic_action_done\"\n          android:alphabeticShortcut=\"s\"\n          app:showAsAction=\"always\"/>\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/profile_share_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:id=\"@+id/action_group_qr\"\n        android:title=\"@string/share_qr_nfc\">\n        <menu>\n            <item\n                android:id=\"@+id/action_standard_qr\"\n                android:title=\"@string/standard\" />\n            <item\n                android:id=\"@+id/action_universal_qr\"\n                android:title=\"@string/app_name\" />\n            <item\n                android:id=\"@+id/action_v2rayn_qr\"\n                android:title=\"@string/v2rayn\" />\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_group_clipboard\"\n        android:title=\"@string/action_export_clipboard\">\n        <menu>\n            <item\n                android:id=\"@+id/action_standard_clipboard\"\n                android:title=\"@string/standard\" />\n            <item\n                android:id=\"@+id/action_universal_clipboard\"\n                android:title=\"@string/app_name\" />\n            <item\n                android:id=\"@+id/action_v2rayn_clipboard\"\n                android:title=\"@string/v2rayn\" />\n        </menu>\n    </item>\n    <item\n        android:id=\"@+id/action_group_configuration\"\n\n        android:title=\"@string/menu_configuration\">\n        <menu>\n            <item\n                android:id=\"@+id/action_config_export_clipboard\"\n                android:title=\"@string/action_export_clipboard\" />\n            <item\n                android:id=\"@+id/action_config_export_file\"\n                android:title=\"@string/action_export_file\" />\n        </menu>\n    </item>\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/scanner_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item android:id=\"@+id/action_import_file\"\n          android:title=\"@string/action_import_file\"\n          android:icon=\"@drawable/ic_image_photo\"\n          android:alphabeticShortcut=\"o\"\n          app:showAsAction=\"always\"/>\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/traffic_item_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:title=\"@string/action_copy\">\n        <menu>\n            <item\n                android:id=\"@+id/copy_label\"\n                android:title=\"@string/copy_label\" />\n            <item\n                android:id=\"@+id/copy_package_name\"\n                android:title=\"@string/copy_package_name\" />\n        </menu>\n    </item>\n    <item android:title=\"@string/action_open\">\n        <menu>\n            <item\n                android:id=\"@+id/open_app\"\n                android:title=\"@string/open_app\" />\n            <item\n                android:id=\"@+id/open_settings\"\n                android:title=\"@string/open_settings\" />\n            <item\n                android:id=\"@+id/open_market\"\n                android:title=\"@string/open_market\" />\n        </menu>\n    </item>\n\n    <item\n        android:id=\"@+id/create_rule\"\n        android:title=\"@string/create_rule\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/traffic_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/action_clear_traffic_statistics\"\n        android:title=\"@string/clear_traffic_statistics\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/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</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.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</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/raw/insecure.txt",
    "content": "The configuration (insecure) can be detected and identified, the transmission is fully visible to the censor and is not resistant to man-in-the-middle tampering with the content of the communication."
  },
  {
    "path": "app/src/main/res/raw/mkcp_no_seed.txt",
    "content": "This configuration (mKCP + no seed) can be accurately identified packet by packet at a very low cost."
  },
  {
    "path": "app/src/main/res/raw/not_encrypted.txt",
    "content": "This configuration (not encrypted) is extremely easy to detect and identify, the transmission is fully visible to the censor, and there is no resistance to man-in-the-middle tampering with the content of the communication."
  },
  {
    "path": "app/src/main/res/raw/shadowsocks_stream_cipher.txt",
    "content": "This configuration (Shadowsocks streaming cipher) can be accurately proactively detected and decrypted by censors without requiring a password, and cannot be mitigated by turning on IV replay filters on the server side.\n\nLearn more: https://github.com/net4people/bbs/issues/24"
  },
  {
    "path": "app/src/main/res/raw/shadowsocksr.txt",
    "content": "This configuration (ShadowsocksR) has questionable tamper resistance, questionable stealth, questionable security, and the upstream client shadowsocksr-libev has no longer been maintained for almost 5 years."
  },
  {
    "path": "app/src/main/res/raw/vmess_md5_auth.txt",
    "content": "This configuration (VMess MD5 authentication) has been deprecated by upstream because of its questionable resistance to tampering and concealment.\n\nAs of January 1, 2022, compatibility with MD5 authentication information will be disabled on the server side by default. Any client using MD5 authentication information will not be able to connect to a server with VMess MD5 authentication information disabled."
  },
  {
    "path": "app/src/main/res/raw-zh-rCN/insecure.txt",
    "content": "该配置 (不安全) 能够被检测识别，传输的内容对审查者完全可见，并且无法抵抗中间人篡改通讯内容."
  },
  {
    "path": "app/src/main/res/raw-zh-rCN/mkcp_no_seed.txt",
    "content": "该配置 (mKCP + 无 seed) 可以以极低代价被逐包精确识别."
  },
  {
    "path": "app/src/main/res/raw-zh-rCN/not_encrypted.txt",
    "content": "该配置 (未加密) 极易被检测识别，传输的内容对审查者完全可见，并且无法抵抗中间人篡改通讯内容."
  },
  {
    "path": "app/src/main/res/raw-zh-rCN/shadowsocks_stream_cipher.txt",
    "content": "该配置 (Shadowsocks 流式密码) 可以被准确地主动探测、在不需要密码的情况下被审查者解密流量, 且服务端开启 IV 重放过滤器也无法缓解.\n\n了解更多: https://github.com/net4people/bbs/issues/24"
  },
  {
    "path": "app/src/main/res/raw-zh-rCN/shadowsocksr.txt",
    "content": "该配置 (ShadowsocksR) 抗篡改能力存疑, 隐蔽性存疑, 安全性存疑, 且上游客户端 shadowsocksr-libev 已停止维护近 5 年."
  },
  {
    "path": "app/src/main/res/raw-zh-rCN/vmess_md5_auth.txt",
    "content": "该配置 (VMess MD5 认证) 抗篡改能力存疑, 隐蔽性存疑, 已被上游废弃.\n\n自 2022 年 1 月 1 日起, 服务器端将默认禁用对于 MD5 认证信息 的兼容. 任何使用 MD5 认证信息的客户端将无法连接到禁用 VMess MD5 认证信息的服务器端."
  },
  {
    "path": "app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string-array name=\"bypass_private_route\">\n        <item>1.0.0.0/8</item>\n        <item>2.0.0.0/7</item>\n        <item>4.0.0.0/6</item>\n        <item>8.0.0.0/7</item>\n        <item>11.0.0.0/8</item>\n        <item>12.0.0.0/6</item>\n        <item>16.0.0.0/4</item>\n        <item>32.0.0.0/3</item>\n        <item>64.0.0.0/3</item>\n        <item>96.0.0.0/6</item>\n        <item>100.0.0.0/10</item>\n        <item>100.128.0.0/9</item>\n        <item>101.0.0.0/8</item>\n        <item>102.0.0.0/7</item>\n        <item>104.0.0.0/5</item>\n        <item>112.0.0.0/10</item>\n        <item>112.64.0.0/11</item>\n        <item>112.96.0.0/12</item>\n        <item>112.112.0.0/13</item>\n        <item>112.120.0.0/14</item>\n        <item>112.124.0.0/19</item>\n        <item>112.124.32.0/21</item>\n        <item>112.124.40.0/22</item>\n        <item>112.124.44.0/23</item>\n        <item>112.124.46.0/24</item>\n        <item>112.124.48.0/20</item>\n        <item>112.124.64.0/18</item>\n        <item>112.124.128.0/17</item>\n        <item>112.125.0.0/16</item>\n        <item>112.126.0.0/15</item>\n        <item>112.128.0.0/9</item>\n        <item>113.0.0.0/8</item>\n        <item>114.0.0.0/10</item>\n        <item>114.64.0.0/11</item>\n        <item>114.96.0.0/12</item>\n        <item>114.112.0.0/15</item>\n        <item>114.114.0.0/18</item>\n        <item>114.114.64.0/19</item>\n        <item>114.114.96.0/20</item>\n        <item>114.114.112.0/23</item>\n        <item>114.114.115.0/24</item>\n        <item>114.114.116.0/22</item>\n        <item>114.114.120.0/21</item>\n        <item>114.114.128.0/17</item>\n        <item>114.115.0.0/16</item>\n        <item>114.116.0.0/14</item>\n        <item>114.120.0.0/13</item>\n        <item>114.128.0.0/9</item>\n        <item>115.0.0.0/8</item>\n        <item>116.0.0.0/6</item>\n        <item>120.0.0.0/6</item>\n        <item>124.0.0.0/7</item>\n        <item>126.0.0.0/8</item>\n        <item>128.0.0.0/3</item>\n        <item>160.0.0.0/5</item>\n        <item>168.0.0.0/8</item>\n        <item>169.0.0.0/9</item>\n        <item>169.128.0.0/10</item>\n        <item>169.192.0.0/11</item>\n        <item>169.224.0.0/12</item>\n        <item>169.240.0.0/13</item>\n        <item>169.248.0.0/14</item>\n        <item>169.252.0.0/15</item>\n        <item>169.255.0.0/16</item>\n        <item>170.0.0.0/7</item>\n        <item>172.0.0.0/12</item>\n        <item>172.32.0.0/11</item>\n        <item>172.64.0.0/10</item>\n        <item>172.128.0.0/9</item>\n        <item>173.0.0.0/8</item>\n        <item>174.0.0.0/7</item>\n        <item>176.0.0.0/4</item>\n        <item>192.0.0.8/29</item>\n        <item>192.0.0.16/28</item>\n        <item>192.0.0.32/27</item>\n        <item>192.0.0.64/26</item>\n        <item>192.0.0.128/25</item>\n        <item>192.0.1.0/24</item>\n        <item>192.0.3.0/24</item>\n        <item>192.0.4.0/22</item>\n        <item>192.0.8.0/21</item>\n        <item>192.0.16.0/20</item>\n        <item>192.0.32.0/19</item>\n        <item>192.0.64.0/18</item>\n        <item>192.0.128.0/17</item>\n        <item>192.1.0.0/16</item>\n        <item>192.2.0.0/15</item>\n        <item>192.4.0.0/14</item>\n        <item>192.8.0.0/13</item>\n        <item>192.16.0.0/12</item>\n        <item>192.32.0.0/11</item>\n        <item>192.64.0.0/12</item>\n        <item>192.80.0.0/13</item>\n        <item>192.88.0.0/18</item>\n        <item>192.88.64.0/19</item>\n        <item>192.88.96.0/23</item>\n        <item>192.88.98.0/24</item>\n        <item>192.88.100.0/22</item>\n        <item>192.88.104.0/21</item>\n        <item>192.88.112.0/20</item>\n        <item>192.88.128.0/17</item>\n        <item>192.89.0.0/16</item>\n        <item>192.90.0.0/15</item>\n        <item>192.92.0.0/14</item>\n        <item>192.96.0.0/11</item>\n        <item>192.128.0.0/11</item>\n        <item>192.160.0.0/13</item>\n        <item>192.169.0.0/16</item>\n        <item>192.170.0.0/15</item>\n        <item>192.172.0.0/14</item>\n        <item>192.176.0.0/12</item>\n        <item>192.192.0.0/10</item>\n        <item>193.0.0.0/8</item>\n        <item>194.0.0.0/7</item>\n        <item>196.0.0.0/7</item>\n        <item>198.0.0.0/12</item>\n        <item>198.16.0.0/15</item>\n        <item>198.20.0.0/14</item>\n        <item>198.24.0.0/13</item>\n        <item>198.32.0.0/12</item>\n        <item>198.48.0.0/15</item>\n        <item>198.50.0.0/16</item>\n        <item>198.51.0.0/18</item>\n        <item>198.51.64.0/19</item>\n        <item>198.51.96.0/22</item>\n        <item>198.51.101.0/24</item>\n        <item>198.51.102.0/23</item>\n        <item>198.51.104.0/21</item>\n        <item>198.51.112.0/20</item>\n        <item>198.51.128.0/17</item>\n        <item>198.52.0.0/14</item>\n        <item>198.56.0.0/13</item>\n        <item>198.64.0.0/10</item>\n        <item>198.128.0.0/9</item>\n        <item>199.0.0.0/8</item>\n        <item>200.0.0.0/7</item>\n        <item>202.0.0.0/8</item>\n        <item>203.0.0.0/18</item>\n        <item>203.0.64.0/19</item>\n        <item>203.0.96.0/20</item>\n        <item>203.0.112.0/24</item>\n        <item>203.0.114.0/23</item>\n        <item>203.0.116.0/22</item>\n        <item>203.0.120.0/21</item>\n        <item>203.0.128.0/17</item>\n        <item>203.1.0.0/16</item>\n        <item>203.2.0.0/15</item>\n        <item>203.4.0.0/14</item>\n        <item>203.8.0.0/13</item>\n        <item>203.16.0.0/12</item>\n        <item>203.32.0.0/11</item>\n        <item>203.64.0.0/10</item>\n        <item>203.128.0.0/9</item>\n        <item>204.0.0.0/6</item>\n        <item>208.0.0.0/4</item>\n    </string-array>\n\n    <string-array name=\"enc_method_entry\">\n        <item>NONE</item>\n        <item>AES-128-GCM</item>\n        <item>AES-192-GCM</item>\n        <item>AES-256-GCM</item>\n        <item>CHACHA20-IETF-POLY1305</item>\n        <item>XCHACHA20-IETF-POLY1305</item>\n        <item>RC4</item>\n        <item>RC4-MD5</item>\n        <item>AES-128-CTR</item>\n        <item>AES-192-CTR</item>\n        <item>AES-256-CTR</item>\n        <item>AES-128-CFB</item>\n        <item>AES-192-CFB</item>\n        <item>AES-256-CFB</item>\n        <item>AES-128-CFB8</item>\n        <item>AES-192-CFB8</item>\n        <item>AES-256-CFB8</item>\n        <item>AES-128-OFB</item>\n        <item>AES-192-OFB</item>\n        <item>AES-256-OFB</item>\n        <item>BF-CFB</item>\n        <item>CAST5-CFB</item>\n        <item>DES-CFB</item>\n        <item>IDEA-CFB</item>\n        <item>RC2-CFB</item>\n        <item>SEED-CFB</item>\n        <item>CAMELLIA-128-CFB</item>\n        <item>CAMELLIA-192-CFB</item>\n        <item>CAMELLIA-256-CFB</item>\n        <item>CAMELLIA-128-CFB8</item>\n        <item>CAMELLIA-192-CFB8</item>\n        <item>CAMELLIA-256-CFB8</item>\n        <item>SALSA20</item>\n        <item>CHACHA20</item>\n        <item>CHACHA20-IETF</item>\n        <item>XCHACHA20</item>\n    </string-array>\n\n    <string-array name=\"enc_method_value\">\n        <item>none</item>\n        <item>aes-128-gcm</item>\n        <item>aes-192-gcm</item>\n        <item>aes-256-gcm</item>\n        <item>chacha20-ietf-poly1305</item>\n        <item>xchacha20-ietf-poly1305</item>\n        <item>rc4</item>\n        <item>rc4-md5</item>\n        <item>aes-128-ctr</item>\n        <item>aes-192-ctr</item>\n        <item>aes-256-ctr</item>\n        <item>aes-128-cfb</item>\n        <item>aes-192-cfb</item>\n        <item>aes-256-cfb</item>\n        <item>aes-128-cfb8</item>\n        <item>aes-192-cfb8</item>\n        <item>aes-256-cfb8</item>\n        <item>aes-128-ofb</item>\n        <item>aes-192-ofb</item>\n        <item>aes-256-ofb</item>\n        <item>bf-cfb</item>\n        <item>cast5-cfb</item>\n        <item>des-cfb</item>\n        <item>idea-cfb</item>\n        <item>rc2-cfb</item>\n        <item>seed-cfb</item>\n        <item>camellia-128-cfb</item>\n        <item>camellia-192-cfb</item>\n        <item>camellia-256-cfb</item>\n        <item>camellia-128-cfb8</item>\n        <item>camellia-192-cfb8</item>\n        <item>camellia-256-cfb8</item>\n        <item>salsa20</item>\n        <item>chacha20</item>\n        <item>chacha20-ietf</item>\n        <item>xchacha20</item>\n    </string-array>\n\n    <string-array name=\"service_modes\">\n        <item>@string/service_mode_vpn</item>\n        <item>@string/service_mode_proxy</item>\n        <!--\n                <item>@string/service_mode_transproxy</item>\n        -->\n    </string-array>\n    <string-array name=\"service_mode_values\">\n        <item>vpn</item>\n        <item>proxy</item>\n        <!--\n                <item>transproxy</item>\n        -->\n    </string-array>\n\n    <string-array name=\"route_protocol_entry\">\n        <item>TCP and UDP</item>\n        <item>TCP</item>\n        <item>UDP</item>\n    </string-array>\n    <string-array name=\"route_protocol_value\">\n        <item />\n        <item>tcp</item>\n        <item>udp</item>\n    </string-array>\n    <string-array name=\"outbound_entry\">\n        <item>@string/route_proxy</item>\n        <item>@string/route_bypass</item>\n        <item>@string/route_block</item>\n        <item>@string/route_profile</item>\n    </string-array>\n    <string-array name=\"outbound_value\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n    </string-array>\n    <string-array name=\"enc_method_entry_ssr\">\n        <item>NONE</item>\n        <item>RC4</item>\n        <item>RC4-MD5</item>\n        <item>AES-128-CTR</item>\n        <item>AES-192-CTR</item>\n        <item>AES-256-CTR</item>\n        <item>AES-128-CFB</item>\n        <item>AES-192-CFB</item>\n        <item>AES-256-CFB</item>\n        <item>AES-128-CFB8</item>\n        <item>AES-192-CFB8</item>\n        <item>AES-256-CFB8</item>\n        <item>AES-128-OFB</item>\n        <item>AES-192-OFB</item>\n        <item>AES-256-OFB</item>\n        <item>BF-CFB</item>\n        <item>CAST5-CFB</item>\n        <item>DES-CFB</item>\n        <item>IDEA-CFB</item>\n        <item>RC2-CFB</item>\n        <item>SEED-CFB</item>\n        <item>CAMELLIA-128-CFB</item>\n        <item>CAMELLIA-192-CFB</item>\n        <item>CAMELLIA-256-CFB</item>\n        <item>CAMELLIA-128-CFB8</item>\n        <item>CAMELLIA-192-CFB8</item>\n        <item>CAMELLIA-256-CFB8</item>\n        <item>SALSA20</item>\n        <item>CHACHA20</item>\n        <item>CHACHA20-IETF</item>\n        <item>XCHACHA20</item>\n    </string-array>\n\n    <string-array name=\"enc_method_value_ssr\">\n        <item>none</item>\n        <item>rc4</item>\n        <item>rc4-md5</item>\n        <item>aes-128-ctr</item>\n        <item>aes-192-ctr</item>\n        <item>aes-256-ctr</item>\n        <item>aes-128-cfb</item>\n        <item>aes-192-cfb</item>\n        <item>aes-256-cfb</item>\n        <item>aes-128-cfb8</item>\n        <item>aes-192-cfb8</item>\n        <item>aes-256-cfb8</item>\n        <item>aes-128-ofb</item>\n        <item>aes-192-ofb</item>\n        <item>aes-256-ofb</item>\n        <item>bf-cfb</item>\n        <item>cast5-cfb</item>\n        <item>des-cfb</item>\n        <item>idea-cfb</item>\n        <item>rc2-cfb</item>\n        <item>seed-cfb</item>\n        <item>camellia-128-cfb</item>\n        <item>camellia-192-cfb</item>\n        <item>camellia-256-cfb</item>\n        <item>camellia-128-cfb8</item>\n        <item>camellia-192-cfb8</item>\n        <item>camellia-256-cfb8</item>\n        <item>salsa20</item>\n        <item>chacha20</item>\n        <item>chacha20-ietf</item>\n        <item>xchacha20</item>\n    </string-array>\n\n    <string-array name=\"protocol_entry\">\n        <item>ORIGIN</item>\n        <item>AUTH_SHA1_V4</item>\n        <item>AUTH_AES128_SHA1</item>\n        <item>AUTH_AES128_MD5</item>\n        <item>AUTH_CHAIN_A</item>\n        <item>AUTH_CHAIN_B</item>\n    </string-array>\n\n    <string-array name=\"protocol_value\">\n        <item>origin</item>\n        <item>auth_sha1_v4</item>\n        <item>auth_aes128_sha1</item>\n        <item>auth_aes128_md5</item>\n        <item>auth_chain_a</item>\n        <item>auth_chain_b</item>\n    </string-array>\n\n    <string-array name=\"obfs_entry\">\n        <item>PLAIN</item>\n        <item>HTTP_SIMPLE</item>\n        <item>HTTP_POST</item>\n        <item>TLS_SIMPLE</item>\n        <item>TLS1.2_TICKET_AUTH</item>\n        <item>TLS1.2_TICKET_FASTAUTH</item>\n        <item>RANDOM_HEAD</item>\n    </string-array>\n\n    <string-array name=\"obfs_value\">\n        <item>plain</item>\n        <item>http_simple</item>\n        <item>http_post</item>\n        <item>tls_simple</item>\n        <item>tls1.2_ticket_auth</item>\n        <item>tls1.2_ticket_fastauth</item>\n        <item>random_head</item>\n    </string-array>\n\n    <string-array name=\"networks_entry\">\n        <item>TCP</item>\n        <item>KCP</item>\n        <item>WS</item>\n        <item>H2</item>\n        <item>QUIC</item>\n        <item>GRPC</item>\n    </string-array>\n\n    <string-array name=\"networks_value\">\n        <item>tcp</item>\n        <item>kcp</item>\n        <item>ws</item>\n        <item>http</item>\n        <item>quic</item>\n        <item>grpc</item>\n    </string-array>\n\n    <string-array name=\"trojan_go_networks_entry\">\n        <item>NONE</item>\n        <item>WEBSOCKET</item>\n    </string-array>\n\n    <string-array name=\"trojan_go_networks_value\">\n        <item>none</item>\n        <item>ws</item>\n    </string-array>\n\n    <string-array name=\"tcp_headers_entry\">\n        <item>NONE</item>\n        <item>HTTP</item>\n    </string-array>\n\n    <string-array name=\"trojan_go_security_entry\">\n        <item>NONE</item>\n        <item>SHADOWSOCKS</item>\n    </string-array>\n\n    <string-array name=\"trojan_go_security_value\">\n        <item>none</item>\n        <item>ss</item>\n    </string-array>\n\n    <string-array name=\"trojan_go_methods\">\n        <item>AES-128-GCM</item>\n        <item>AES-256-GCM</item>\n        <item>CHACHA20-IETF-POLY1305</item>\n    </string-array>\n\n    <string-array name=\"tcp_headers_value\">\n        <item>none</item>\n        <item>http</item>\n    </string-array>\n\n    <string-array name=\"kcp_quic_headers_entry\">\n        <item>NONE</item>\n        <item>SRTP</item>\n        <item>UTP</item>\n        <item>WECHAT-VIDEO</item>\n        <item>DTLS</item>\n        <item>WIREGUARD</item>\n    </string-array>\n\n    <string-array name=\"kcp_quic_headers_value\">\n        <item>none</item>\n        <item>srtp</item>\n        <item>utp</item>\n        <item>wechat-video</item>\n        <item>dtls</item>\n        <item>wireguard</item>\n    </string-array>\n\n    <string-array name=\"quic_security_entry\">\n        <item>CHACHA20-POLY1305</item>\n        <item>AES-128-GCM</item>\n        <item>NONE</item>\n    </string-array>\n\n    <string-array name=\"quic_security_value\">\n        <item>chacha20-poly1305</item>\n        <item>aes-128-gcm</item>\n        <item>none</item>\n    </string-array>\n\n    <string-array name=\"vmess_encryption_entry\">\n        <item>CHACHA20-POLY1305</item>\n        <item>AES-128-GCM</item>\n        <item>AUTO</item>\n        <item>NONE</item>\n        <item>ZERO</item>\n    </string-array>\n\n    <string-array name=\"vmess_encryption_value\">\n        <item>chacha20-poly1305</item>\n        <item>aes-128-gcm</item>\n        <item>auto</item>\n        <item>none</item>\n        <item>zero</item>\n    </string-array>\n\n    <string-array name=\"vless_encryption_entry\">\n        <item>NONE</item>\n    </string-array>\n\n    <string-array name=\"vless_encryption_value\">\n        <item>none</item>\n    </string-array>\n\n    <string-array name=\"transport_layer_encryption_entry\">\n        <item>NONE</item>\n        <item>TLS</item>\n        <item>XTLS</item>\n\n    </string-array>\n\n    <string-array name=\"transport_layer_encryption_value\">\n        <item>none</item>\n        <item>tls</item>\n        <item>xtls</item>\n    </string-array>\n\n    <string-array name=\"trojan_transport_layer_encryption_entry\">\n        <item>TLS</item>\n        <item>XTLS</item>\n    </string-array>\n\n    <string-array name=\"trojan_transport_layer_encryption_value\">\n        <item>tls</item>\n        <item>xtls</item>\n    </string-array>\n\n    <string-array name=\"xtls_flow_entry\" translatable=\"false\">\n        <item>XTLS-RPRX-ORIGIN</item>\n        <item>XTLS-RPRX-ORIGIN-UDP443</item>\n        <item>XTLS-RPRX-DIRECT</item>\n        <item>XTLS-RPRX-DIRECT-UDP443</item>\n        <item>XTLS-RPRX-SPLICE</item>\n        <item>XTLS-RPRX-SPLICE-UDP443</item>\n    </string-array>\n\n    <string-array name=\"xtls_flow_value\" translatable=\"false\">\n        <item>xtls-rprx-origin</item>\n        <item>xtls-rprx-origin-udp443</item>\n        <item>xtls-rprx-direct</item>\n        <item>xtls-rprx-direct-udp443</item>\n        <item>xtls-rprx-splice</item>\n        <item>xtls-rprx-splice-udp443</item>\n    </string-array>\n\n    <string-array name=\"notification_entry\">\n        <item>@string/disable</item>\n        <item>500ms</item>\n        <item>1s</item>\n        <item>3s</item>\n        <item>10s</item>\n    </string-array>\n\n    <string-array name=\"notification_value\">\n        <item>0</item>\n        <item>500</item>\n        <item>1000</item>\n        <item>3000</item>\n        <item>10000</item>\n    </string-array>\n\n    <string-array name=\"domain_strategy\">\n        <item>AsIs</item>\n        <item>IPIfNonMatch</item>\n        <item>IPOnDemand</item>\n    </string-array>\n\n    <string-array name=\"transproxy_mode\">\n        <item>REDIRECT</item>\n        <item>TPROXY</item>\n    </string-array>\n\n    <string-array name=\"int_array_2\">\n        <item>0</item>\n        <item>1</item>\n    </string-array>\n\n    <string-array name=\"naive_proto_entry\">\n        <item>HTTPS</item>\n        <item>QUIC</item>\n    </string-array>\n\n    <string-array name=\"naive_proto_value\">\n        <item>https</item>\n        <item>quic</item>\n    </string-array>\n\n    <string-array name=\"brook_protocol_entry\">\n        <item>DEFAULT</item>\n        <item>WS</item>\n        <item>WSS</item>\n    </string-array>\n    <string-array name=\"brook_protocol_value\">\n        <item />\n        <item>ws</item>\n        <item>wss</item>\n    </string-array>\n\n    <string-array name=\"config_type_entry\">\n        <item>Xray</item>\n        <item>Trojan-Go</item>\n    </string-array>\n    <string-array name=\"config_type_value\">\n        <item>v2ray</item>\n        <item>trojan-go</item>\n    </string-array>\n\n    <string-array name=\"night_mode\">\n        <item>@string/follow_system</item>\n        <item>@string/enable</item>\n        <item>@string/disable</item>\n        <item>@string/auto</item>\n    </string-array>\n    <string-array name=\"int_array_4\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n    </string-array>\n\n    <string-array name=\"ipv6_mode\">\n        <item>@string/disable</item>\n        <item>@string/enable</item>\n        <item>@string/prefer</item>\n        <item>@string/only</item>\n    </string-array>\n\n    <string-array name=\"rules_dat_provider\">\n        <item>@string/route_rules_official</item>\n        <item>Loyalsoldier/v2ray-rules-dat</item>\n    </string-array>\n\n    <string-array name=\"balancer_type\">\n        <item>@string/list</item>\n        <item>@string/menu_group</item>\n    </string-array>\n\n    <string-array name=\"balancer_strategy_entry\">\n        <item>@string/random</item>\n        <item>@string/leastPing</item>\n    </string-array>\n    <string-array name=\"balancer_strategy_value\">\n        <item />\n        <item>leastPing</item>\n    </string-array>\n\n    <string-array name=\"int_array_3\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"trojan_provider\">\n        <item>Xray</item>\n        <item>Trojan-Go</item>\n    </string-array>\n\n    <string-array name=\"trojan_provider_value\">\n        <item>0</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"trojan_provider_experimental\">\n        <item>Xray</item>\n        <item>Trojan</item>\n        <item>Trojan-Go</item>\n    </string-array>\n\n    <string-array name=\"ss_aead_provider\">\n        <item>Xray</item>\n        <item>shadowsocks-rust</item>\n        <item>shadowsocks-libev</item>\n        <item>Clash</item>\n    </string-array>\n    <string-array name=\"ss_aead_provider_values\">\n        <item>0</item>\n        <item>1</item>\n        <item>3</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"ss_aead_provider_api21\">\n        <item>V2Ray</item>\n        <item>shadowsocks-libev</item>\n        <item>Clash</item>\n    </string-array>\n    <string-array name=\"ss_aead_provider_api21_values\">\n        <item>0</item>\n        <item>3</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"ss_stream_provider\">\n        <item>shadowsocks-rust</item>\n        <item>shadowsocks-libev</item>\n        <item>Clash</item>\n    </string-array>\n\n    <string-array name=\"ss_stream_provider_values\">\n        <item>0</item>\n        <item>2</item>\n        <item>1</item>\n    </string-array>\n\n    <string-array name=\"ss_stream_provider_api21\">\n        <item>shadowsocks-libev</item>\n        <item>Clash</item>\n    </string-array>\n\n    <string-array name=\"ss_stream_provider_api21_values\">\n        <item>2</item>\n        <item>1</item>\n    </string-array>\n\n    <string-array name=\"group_types\">\n        <item>@string/group_basic</item>\n        <item>@string/subscription</item>\n    </string-array>\n\n    <string-array name=\"subscription_types\">\n        <item>@string/raw</item>\n        <item>OOCv1</item>\n        <item>SIP008</item>\n    </string-array>\n\n    <string-array name=\"hysteria_auth_type\">\n        <item>@string/plugin_disabled</item>\n        <item>STRING</item>\n        <item>BASE64</item>\n    </string-array>\n\n    <string-array name=\"socks_versions\">\n        <item>SOCKS4</item>\n        <item>SOCKS4A</item>\n        <item>SOCKS5</item>\n    </string-array>\n\n    <string-array name=\"utls_entry\">\n        <item>DISABLED</item>\n        <item>CHROME</item>\n        <item>FIREFOX</item>\n        <item>SAFARI</item>\n        <item>RANDOMIZED</item>\n    </string-array>\n\n    <string-array name=\"utls_value\">\n        <item />\n        <item>chrome</item>\n        <item>firefox</item>\n        <item>safari</item>\n        <item>randomized</item>\n    </string-array>\n\n    <string-array name=\"snell_versions\">\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"simple_obfs\">\n        <item>http</item>\n        <item>tls</item>\n    </string-array>\n\n    <string-array name=\"group_orders\">\n        <item>@string/group_order_origin</item>\n        <item>@string/group_order_by_name</item>\n        <item>@string/group_order_by_delay</item>\n    </string-array>\n\n    <string-array name=\"foreground_status\">\n        <item tools:ignore=\"PrivateResource\">@string/not_set</item>\n        <item>@string/foreground</item>\n        <item>@string/background</item>\n    </string-array>\n\n    <string-array name=\"foreground_status_value\">\n        <item />\n        <item>foreground</item>\n        <item>background</item>\n    </string-array>\n\n    <string-array name=\"ssh_auth_type\">\n        <item>@string/ssh_auth_type_none</item>\n        <item>@string/password</item>\n        <item>@string/ssh_public_key</item>\n    </string-array>\n\n    <string-array name=\"tun_implementation\">\n        <item>gVisor</item>\n        <item>LwIP</item>\n    </string-array>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~\n  ~ Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>\n  ~ Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>\n  ~ Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>\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 <http://www.gnu.org/licenses/>.\n  ~\n  -->\n\n<resources>\n    <attr name=\"colorMaterial100\" format=\"color\" />\n    <attr name=\"colorMaterial300\" format=\"color\" />\n    <attr name=\"accentOrTextSecondary\" format=\"color\" />\n    <attr name=\"accentOrTextPrimary\" format=\"color\" />\n    <attr name=\"primaryOrTextSecondary\" format=\"color\" />\n    <attr name=\"primaryOrTextPrimary\" format=\"color\" />\n    <attr name=\"fabColorBackground\" format=\"color\" />\n    <attr name=\"selectedColorPrimary\" format=\"color\" />\n    <attr name=\"whiteOrTextPrimary\" format=\"color\" />\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <integer-array name=\"material_colors\">\n        <item>@color/material_red_500</item>\n        <item>@color/material_pink_500</item>\n        <item>@color/material_purple_500</item>\n        <item>@color/material_deep_purple_500</item>\n        <item>@color/material_indigo_500</item>\n        <item>@color/material_blue_500</item>\n        <item>@color/material_light_blue_500</item>\n        <item>@color/material_cyan_500</item>\n        <item>@color/material_teal_500</item>\n        <item>@color/material_green_500</item>\n\n        <item>@color/material_light_green_500</item>\n        <item>@color/material_lime_500</item>\n        <item>@color/material_yellow_500</item>\n        <item>@color/material_amber_500</item>\n        <item>@color/material_orange_500</item>\n        <item>@color/material_deep_orange_500</item>\n\n        <item>@color/material_brown_500</item>\n        <item>@color/material_grey_500</item>\n        <item>@color/material_blue_grey_500</item>\n        <item>@color/material_light_black</item>\n    </integer-array>\n\n    <color name=\"material_amber_100\">#FFECB3</color>\n    <color name=\"material_amber_200\">#FFE082</color>\n    <color name=\"material_amber_300\">#FFD54F</color>\n    <color name=\"material_amber_400\">#FFCA28</color>\n    <color name=\"material_amber_50\">#FFF8E1</color>\n    <color name=\"material_amber_500\">#FFC107</color>\n    <color name=\"material_amber_600\">#FFB300</color>\n    <color name=\"material_amber_700\">#FFA000</color>\n    <color name=\"material_amber_800\">#FF8F00</color>\n    <color name=\"material_amber_900\">#FF6F00</color>\n    <color name=\"material_amber_accent_100\">#FFE57F</color>\n    <color name=\"material_amber_accent_200\">#FFD740</color>\n    <color name=\"material_amber_accent_400\">#FFC400</color>\n    <color name=\"material_amber_accent_700\">#FFAB00</color>\n    <color name=\"material_blue_100\">#BBDEFB</color>\n    <color name=\"material_blue_200\">#90CAF9</color>\n    <color name=\"material_blue_300\">#64B5F6</color>\n    <color name=\"material_blue_400\">#42A5F5</color>\n    <color name=\"material_blue_50\">#E3F2FD</color>\n    <color name=\"material_blue_500\">#2196F3</color>\n    <color name=\"material_blue_600\">#1E88E5</color>\n    <color name=\"material_blue_700\">#1976D2</color>\n    <color name=\"material_blue_800\">#1565C0</color>\n    <color name=\"material_blue_900\">#0D47A1</color>\n    <color name=\"material_blue_accent_100\">#82B1FF</color>\n    <color name=\"material_blue_accent_200\">#448AFF</color>\n    <color name=\"material_blue_accent_400\">#2979FF</color>\n    <color name=\"material_blue_accent_700\">#2962FF</color>\n    <color name=\"material_blue_grey_100\">#CFD8DC</color>\n    <color name=\"material_blue_grey_200\">#B0BEC5</color>\n    <color name=\"material_blue_grey_300\">#90A4AE</color>\n    <color name=\"material_blue_grey_400\">#78909C</color>\n    <color name=\"material_blue_grey_50\">#ECEFF1</color>\n    <color name=\"material_blue_grey_500\">#607D8B</color>\n    <color name=\"material_blue_grey_600\">#546E7A</color>\n    <color name=\"material_blue_grey_700\">#455A64</color>\n    <color name=\"material_blue_grey_800\" tools:override=\"true\">#37474F</color>\n    <color name=\"material_blue_grey_900\" tools:override=\"true\">#263238</color>\n    <color name=\"material_brown_100\">#D7CCC8</color>\n    <color name=\"material_brown_200\">#BCAAA4</color>\n    <color name=\"material_brown_300\">#A1887F</color>\n    <color name=\"material_brown_400\">#8D6E63</color>\n    <color name=\"material_brown_50\">#EFEBE9</color>\n    <color name=\"material_brown_500\">#795548</color>\n    <color name=\"material_brown_600\">#6D4C41</color>\n    <color name=\"material_brown_700\">#5D4037</color>\n    <color name=\"material_brown_800\">#4E342E</color>\n    <color name=\"material_brown_900\">#3E2723</color>\n    <color name=\"material_cyan_100\">#B2EBF2</color>\n    <color name=\"material_cyan_200\">#80DEEA</color>\n    <color name=\"material_cyan_300\">#4DD0E1</color>\n    <color name=\"material_cyan_400\">#26C6DA</color>\n    <color name=\"material_cyan_50\">#E0F7FA</color>\n    <color name=\"material_cyan_500\">#00BCD4</color>\n    <color name=\"material_cyan_600\">#00ACC1</color>\n    <color name=\"material_cyan_700\">#0097A7</color>\n    <color name=\"material_cyan_800\">#00838F</color>\n    <color name=\"material_cyan_900\">#006064</color>\n    <color name=\"material_cyan_accent_100\">#84FFFF</color>\n    <color name=\"material_cyan_accent_200\">#18FFFF</color>\n    <color name=\"material_cyan_accent_400\">#00E5FF</color>\n    <color name=\"material_cyan_accent_700\">#00B8D4</color>\n    <color name=\"material_deep_orange_100\">#FFCCBC</color>\n    <color name=\"material_deep_orange_200\">#FFAB91</color>\n    <color name=\"material_deep_orange_300\">#FF8A65</color>\n    <color name=\"material_deep_orange_400\">#FF7043</color>\n    <color name=\"material_deep_orange_50\">#FBE9E7</color>\n    <color name=\"material_deep_orange_500\">#FF5722</color>\n    <color name=\"material_deep_orange_600\">#F4511E</color>\n    <color name=\"material_deep_orange_700\">#E64A19</color>\n    <color name=\"material_deep_orange_800\">#D84315</color>\n    <color name=\"material_deep_orange_900\">#BF360C</color>\n    <color name=\"material_deep_orange_accent_100\">#FF9E80</color>\n    <color name=\"material_deep_orange_accent_200\">#FF6E40</color>\n    <color name=\"material_deep_orange_accent_400\">#FF3D00</color>\n    <color name=\"material_deep_orange_accent_700\">#DD2C00</color>\n    <color name=\"material_deep_purple_100\">#D1C4E9</color>\n    <color name=\"material_deep_purple_200\">#B39DDB</color>\n    <color name=\"material_deep_purple_300\">#9575CD</color>\n    <color name=\"material_deep_purple_400\">#7E57C2</color>\n    <color name=\"material_deep_purple_50\">#EDE7F6</color>\n    <color name=\"material_deep_purple_500\">#673AB7</color>\n    <color name=\"material_deep_purple_600\">#5E35B1</color>\n    <color name=\"material_deep_purple_700\">#512DA8</color>\n    <color name=\"material_deep_purple_800\">#4527A0</color>\n    <color name=\"material_deep_purple_900\">#311B92</color>\n    <color name=\"material_deep_purple_accent_100\">#B388FF</color>\n    <color name=\"material_deep_purple_accent_200\">#7C4DFF</color>\n    <color name=\"material_deep_purple_accent_400\">#651FFF</color>\n    <color name=\"material_deep_purple_accent_700\">#6200EA</color>\n    <color name=\"material_green_100\">#C8E6C9</color>\n    <color name=\"material_green_200\">#A5D6A7</color>\n    <color name=\"material_green_300\">#81C784</color>\n    <color name=\"material_green_400\">#66BB6A</color>\n    <color name=\"material_green_50\">#E8F5E9</color>\n    <color name=\"material_green_500\">#4CAF50</color>\n    <color name=\"material_green_600\">#43A047</color>\n    <color name=\"material_green_700\">#388E3C</color>\n    <color name=\"material_green_800\">#2E7D32</color>\n    <color name=\"material_green_900\">#1B5E20</color>\n    <color name=\"material_green_accent_100\">#B9F6CA</color>\n    <color name=\"material_green_accent_200\">#69F0AE</color>\n    <color name=\"material_green_accent_400\">#00E676</color>\n    <color name=\"material_green_accent_700\">#00C853</color>\n    <color name=\"material_grey_200\">#EEEEEE</color>\n    <color name=\"material_grey_400\">#BDBDBD</color>\n    <color name=\"material_grey_500\">#9E9E9E</color>\n    <color name=\"material_grey_700\">#616161</color>\n    <color name=\"material_indigo_100\">#C5CAE9</color>\n    <color name=\"material_indigo_200\">#9FA8DA</color>\n    <color name=\"material_indigo_300\">#7986CB</color>\n    <color name=\"material_indigo_400\">#5C6BC0</color>\n    <color name=\"material_indigo_50\">#E8EAF6</color>\n    <color name=\"material_indigo_500\">#3F51B5</color>\n    <color name=\"material_indigo_600\">#3949AB</color>\n    <color name=\"material_indigo_700\">#303F9F</color>\n    <color name=\"material_indigo_800\">#283593</color>\n    <color name=\"material_indigo_900\">#1A237E</color>\n    <color name=\"material_indigo_accent_100\">#8C9EFF</color>\n    <color name=\"material_indigo_accent_200\">#536DFE</color>\n    <color name=\"material_indigo_accent_400\">#3D5AFE</color>\n    <color name=\"material_indigo_accent_700\">#304FFE</color>\n    <color name=\"material_light_black\">#212121</color>\n    <color name=\"material_light_blue_100\">#B3E5FC</color>\n    <color name=\"material_light_blue_200\">#81D4FA</color>\n    <color name=\"material_light_blue_300\">#4FC3F7</color>\n    <color name=\"material_light_blue_400\">#29B6F6</color>\n    <color name=\"material_light_blue_50\">#E1F5FE</color>\n    <color name=\"material_light_blue_500\">#03A9F4</color>\n    <color name=\"material_light_blue_600\">#039BE5</color>\n    <color name=\"material_light_blue_700\">#0288D1</color>\n    <color name=\"material_light_blue_800\">#0277BD</color>\n    <color name=\"material_light_blue_900\">#01579B</color>\n    <color name=\"material_light_blue_accent_100\">#80D8FF</color>\n    <color name=\"material_light_blue_accent_200\">#40C4FF</color>\n    <color name=\"material_light_blue_accent_400\">#00B0FF</color>\n    <color name=\"material_light_blue_accent_700\">#0091EA</color>\n    <color name=\"material_light_green_100\">#DCEDC8</color>\n    <color name=\"material_light_green_200\">#C5E1A5</color>\n    <color name=\"material_light_green_300\">#AED581</color>\n    <color name=\"material_light_green_400\">#9CCC65</color>\n    <color name=\"material_light_green_50\">#F1F8E9</color>\n    <color name=\"material_light_green_500\">#8BC34A</color>\n    <color name=\"material_light_green_600\">#7CB342</color>\n    <color name=\"material_light_green_700\">#689F38</color>\n    <color name=\"material_light_green_800\">#558B2F</color>\n    <color name=\"material_light_green_900\">#33691E</color>\n    <color name=\"material_light_green_accent_100\">#CCFF90</color>\n    <color name=\"material_light_green_accent_200\">#B2FF59</color>\n    <color name=\"material_light_green_accent_400\">#76FF03</color>\n    <color name=\"material_light_green_accent_700\">#64DD17</color>\n    <color name=\"material_light_white\">#eeeeee</color>\n    <color name=\"material_lime_100\">#F0F4C3</color>\n    <color name=\"material_lime_200\">#E6EE9C</color>\n    <color name=\"material_lime_300\">#DCE775</color>\n    <color name=\"material_lime_400\">#D4E157</color>\n    <color name=\"material_lime_50\">#F9FBE7</color>\n    <color name=\"material_lime_500\">#CDDC39</color>\n    <color name=\"material_lime_600\">#C0CA33</color>\n    <color name=\"material_lime_700\">#AFB42B</color>\n    <color name=\"material_lime_800\">#9E9D24</color>\n    <color name=\"material_lime_900\">#827717</color>\n    <color name=\"material_lime_accent_100\">#F4FF81</color>\n    <color name=\"material_lime_accent_200\">#EEFF41</color>\n    <color name=\"material_lime_accent_400\">#C6FF00</color>\n    <color name=\"material_lime_accent_700\">#AEEA00</color>\n    <color name=\"material_orange_100\">#FFE0B2</color>\n    <color name=\"material_orange_200\">#FFCC80</color>\n    <color name=\"material_orange_300\">#FFB74D</color>\n    <color name=\"material_orange_400\">#FFA726</color>\n    <color name=\"material_orange_50\">#FFF3E0</color>\n    <color name=\"material_orange_500\">#FF9800</color>\n    <color name=\"material_orange_600\">#FB8C00</color>\n    <color name=\"material_orange_700\">#F57C00</color>\n    <color name=\"material_orange_800\">#EF6C00</color>\n    <color name=\"material_orange_900\">#E65100</color>\n    <color name=\"material_orange_accent_100\">#FFD180</color>\n    <color name=\"material_orange_accent_200\">#FFAB40</color>\n    <color name=\"material_orange_accent_400\">#FF9100</color>\n    <color name=\"material_orange_accent_700\">#FF6D00</color>\n    <color name=\"material_pink_100\">#F8BBD0</color>\n    <color name=\"material_pink_200\">#F48FB1</color>\n    <color name=\"material_pink_300\">#F06292</color>\n    <color name=\"material_pink_400\">#EC407A</color>\n    <color name=\"material_pink_50\">#FCE4EC</color>\n    <color name=\"material_pink_500\">#E91E63</color>\n    <color name=\"material_pink_600\">#D81B60</color>\n    <color name=\"material_pink_700\">#C2185B</color>\n    <color name=\"material_pink_800\">#AD1457</color>\n    <color name=\"material_pink_900\">#880E4F</color>\n    <color name=\"material_pink_accent_100\">#FF80AB</color>\n    <color name=\"material_pink_accent_200\">#FF4081</color>\n    <color name=\"material_pink_accent_400\">#F50057</color>\n    <color name=\"material_pink_accent_700\">#C51162</color>\n    <color name=\"material_purple_100\">#E1BEE7</color>\n    <color name=\"material_purple_200\">#CE93D8</color>\n    <color name=\"material_purple_300\">#BA68C8</color>\n    <color name=\"material_purple_400\">#AB47BC</color>\n    <color name=\"material_purple_50\">#F3E5F5</color>\n    <color name=\"material_purple_500\">#9C27B0</color>\n    <color name=\"material_purple_600\">#8E24AA</color>\n    <color name=\"material_purple_700\">#7B1FA2</color>\n    <color name=\"material_purple_800\">#6A1B9A</color>\n    <color name=\"material_purple_900\">#4A148C</color>\n    <color name=\"material_purple_accent_100\">#EA80FC</color>\n    <color name=\"material_purple_accent_200\">#E040FB</color>\n    <color name=\"material_purple_accent_400\">#D500F9</color>\n    <color name=\"material_purple_accent_700\">#AA00FF</color>\n    <color name=\"material_red_100\">#FFCDD2</color>\n    <color name=\"material_red_200\">#EF9A9A</color>\n    <color name=\"material_red_300\">#E57373</color>\n    <color name=\"material_red_400\">#EF5350</color>\n    <color name=\"material_red_50\">#FFEBEE</color>\n    <color name=\"material_red_500\">#F44336</color>\n    <color name=\"material_red_600\">#E53935</color>\n    <color name=\"material_red_700\">#D32F2F</color>\n    <color name=\"material_red_800\">#C62828</color>\n    <color name=\"material_red_900\">#B71C1C</color>\n    <color name=\"material_red_accent_100\">#FF8A80</color>\n    <color name=\"material_red_accent_200\">#FF5252</color>\n    <color name=\"material_red_accent_400\">#FF1744</color>\n    <color name=\"material_red_accent_700\">#D50000</color>\n    <color name=\"material_teal_100\">#B2DFDB</color>\n    <color name=\"material_teal_200\">#80CBC4</color>\n    <color name=\"material_teal_300\">#4DB6AC</color>\n    <color name=\"material_teal_400\">#26A69A</color>\n    <color name=\"material_teal_50\">#E0F2F1</color>\n    <color name=\"material_teal_500\">#009688</color>\n    <color name=\"material_teal_600\">#00897B</color>\n    <color name=\"material_teal_700\">#00796B</color>\n    <color name=\"material_teal_800\">#00695C</color>\n    <color name=\"material_teal_900\">#004D40</color>\n    <color name=\"material_teal_accent_100\">#A7FFEB</color>\n    <color name=\"material_teal_accent_200\">#64FFDA</color>\n    <color name=\"material_teal_accent_400\">#1DE9B6</color>\n    <color name=\"material_teal_accent_700\">#00BFA5</color>\n    <color name=\"material_yellow_100\">#FFF9C4</color>\n    <color name=\"material_yellow_200\">#FFF59D</color>\n    <color name=\"material_yellow_300\">#FFF176</color>\n    <color name=\"material_yellow_400\">#FFEE58</color>\n    <color name=\"material_yellow_50\">#FFFDE7</color>\n    <color name=\"material_yellow_500\">#FFEB3B</color>\n    <color name=\"material_yellow_600\">#FDD835</color>\n    <color name=\"material_yellow_700\">#FBC02D</color>\n    <color name=\"material_yellow_800\">#F9A825</color>\n    <color name=\"material_yellow_900\">#F57F17</color>\n    <color name=\"material_yellow_accent_100\">#FFFF8D</color>\n    <color name=\"material_yellow_accent_200\">#FFFF00</color>\n    <color name=\"material_yellow_accent_400\">#FFEA00</color>\n    <color name=\"material_yellow_accent_700\">#FFD600</color>\n\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n\n    <color name=\"fab_color_progress\">@color/material_light_black</color>\n\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <dimen name=\"qrcode_size\">264dp</dimen>\n    <dimen name=\"main_list_padding_bottom\">88dp</dimen>\n    <dimen name=\"bottom_sheet_padding\">8dp</dimen>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">AnXray</string>\n    <string name=\"app_desc\" translatable=\"false\">Another Xray for Android.</string>\n    <string name=\"project\">Project</string>\n    <string name=\"github\">Source code</string>\n    <string name=\"telegram\">Telegram update channel</string>\n    <string name=\"oss_licenses\">Open source licenses</string>\n    <string name=\"app_version\">Version</string>\n    <string name=\"version_x\">Version (%s)</string>\n    <string name=\"logcat\">Export debug information</string>\n    <string name=\"logcat_summary\">Such useful very wow</string>\n    <string name=\"menu_configuration\">Configuration</string>\n    <string name=\"menu_group\">Group</string>\n    <string name=\"menu_about\">About</string>\n    <string name=\"theme\">Theme</string>\n    <string name=\"document\">Document</string>\n    <string name=\"group_default\">Ungrouped</string>\n    <string name=\"quick_toggle\">Toggle</string>\n    <string name=\"tile_title\">Switcher</string>\n    <string name=\"menu_traffic\">Traffic</string>\n    <!-- group -->\n    <string name=\"group_name\">Group name</string>\n    <string name=\"group_update\">Update</string>\n    <string name=\"group_subscription_link\">Subscription Link</string>\n    <string name=\"group_name_required\">Group name required</string>\n    <string name=\"group_create\">Create group</string>\n    <string name=\"group_create_subscription\">Create subscription</string>\n    <string name=\"group_edit\">Edit group</string>\n    <string name=\"group_edit_subscription\">Edit subscription</string>\n    <string name=\"group_status_empty\">Empty group</string>\n    <string name=\"group_status_empty_subscription\">Not updated yet</string>\n    <string name=\"group_status_proxies\">%d Proxies</string>\n    <string name=\"group_status_proxies_subscription\">%d Proxies | Updated on %s</string>\n    <string name=\"group_no_difference\">%s: No difference</string>\n    <string name=\"group_updated\">%s: Updated %d proxies</string>\n    <string name=\"group_show_diff\">Diff</string>\n    <string name=\"group_diff\">Diff (%s)</string>\n    <string name=\"group_delete_confirm_prompt\">Are you sure you want to remove this group?</string>\n    <string name=\"group_added\">Added: \\n%s</string>\n    <string name=\"group_changed\">Updated: \\n%s</string>\n    <string name=\"group_deleted\">Deleted: \\n%s</string>\n    <string name=\"group_duplicate\">Duplicate: \\n%s</string>\n    <!-- misc -->\n    <string name=\"service_mode\">Service Mode</string>\n    <string name=\"service_mode_proxy\">Proxy only</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"port_proxy\">SOCKS5 Proxy Port</string>\n    <string name=\"port_http\">HTTP Proxy Port</string>\n    <string name=\"port_transproxy\">Transproxy Port</string>\n    <string name=\"cag_route\">Route Settings</string>\n    <string name=\"allow_access\">Allow Connections from the LAN</string>\n    <string name=\"allow_access_sum\">Bind inbound servers to 0.0.0.0</string>\n    <string name=\"inbound_settings\">Inbound Settings</string>\n    <string name=\"general_settings\">App Settings</string>\n    <string name=\"require_http\">Enable HTTP inbound</string>\n    <string name=\"cag_ws\">WebSocket Settings</string>\n    <string name=\"ws_max_early_data\" translatable=\"false\">Max early data</string>\n    <string name=\"ws_browser_forwarding\">Browser Forwarding</string>\n    <string name=\"ws_browser_forwarding_sum\">Forward corresponding WebSockets through the browser.</string>\n    <string name=\"speed_interval\">Speed Notification Update Interval</string>\n    <string name=\"cag_misc\">Misc Settings</string>\n    <string name=\"show_stop\">Show Stop Button</string>\n    <string name=\"show_stop_sum\">If you don’t want to use Quick Tile as the switch</string>\n    <string name=\"show_direct_speed\">Show Direct Speed</string>\n    <string name=\"show_direct_speed_sum\">Show the traffic speed without proxy in the notification as well</string>\n    <string name=\"xray_utls_fingerprint\">Xray uTLS Fingerprint</string>\n    <string name=\"security_settings\">Security Settings</string>\n    <string name=\"allow_insecure\">Allow Insecure</string>\n    <string name=\"allow_insecure_sum\">Disable certificate checking. When enabled, this configuration is as secure as plaintext</string>\n    <string name=\"traffic\" translatable=\"false\">%1$s↑ %2$s↓</string>\n    <string name=\"speed_detail\">Proxy： %1$s↑ %2$s↓\\nDirect： %3$s↑ %4$s↓</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"connection_test_testing\">Testing…</string>\n    <string name=\"connection_test_available\">Success: HTTPS handshake took %dms</string>\n    <string name=\"connection_test_available_http\">Success: HTTP handshake took %dms</string>\n    <string name=\"connection_test_error\">Failed: %s</string>\n    <string name=\"connection_test_fail\">Internet Unavailable</string>\n    <string name=\"connection_test_error_status_code\">Error code: #%d</string>\n    <string name=\"cag_dns\">DNS Settings</string>\n    <string name=\"remote_dns\">Remote DNS</string>\n    <string name=\"direct_dns\">Direct DNS</string>\n    <string name=\"enable_dns_routing\">Enable DNS Routing</string>\n    <string name=\"dns_routing_message\">Resolve domains in bypass routes with Direct DNS. Be aware of potential DNS leaks</string>\n    <string name=\"enable_fakedns\">Enable FakeDNS</string>\n    <string name=\"fakedns_message\">May cause other applications need to be restarted to reconnect to the network after proxy stopped</string>\n    <string name=\"dns_hosts\">Domain rewrite</string>\n    <string name=\"port_local_dns\">Local DNS Port</string>\n    <string name=\"require_transproxy\">Enable Transproxy Inbound</string>\n    <string name=\"transproxy_mode\">Transproxy Mode</string>\n    <string name=\"connection_test_url\">Connection Test URL</string>\n    <!-- proxy category -->\n    <string name=\"profile_name\">Profile Name</string>\n    <string name=\"server_address\">Server</string>\n    <string name=\"server_port\">Remote Port</string>\n    <string name=\"username\">Username</string>\n    <string name=\"username_opt\">Username (Optional)</string>\n    <string name=\"password_opt\">Password (Optional)</string>\n    <string name=\"key_opt\">Key (Optional)</string>\n    <string name=\"password\">Password</string>\n    <string name=\"enc_method\">Encrypt Method</string>\n    <string name=\"protocol\">Protocol</string>\n    <string name=\"protocol_param\">Protocol Param</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"obfs_param\">Obfs Param</string>\n    <string name=\"uuid\">User ID</string>\n    <string name=\"alter_id\">Alter ID</string>\n    <string name=\"security\">Transport layer encryption</string>\n    <string name=\"network\">Network</string>\n    <string name=\"flow\">Flow control algorithm</string>\n\n    <string name=\"header_type\">Header Type</string>\n    <string name=\"ws_host\">WebSocket Host</string>\n    <string name=\"ws_path\">WebSocket Path</string>\n    <string name=\"http_host\">HTTP Host</string>\n    <string name=\"http_path\">HTTP Path</string>\n    <string name=\"quic_security\">QUIC Security</string>\n    <string name=\"quic_key\">Quic KEY</string>\n    <string name=\"kcp_seed\">mKCP Seed</string>\n    <string name=\"grpc_service_name\">gRPC ServiceName</string>\n    <string name=\"multi_mode\">gRPC Multi Mode</string>\n    <string name=\"multi_mode_sum\">Experimental option. May not be retained for a long time, and cross-version compatibility is not guaranteed.</string>\n\n    <string name=\"tls\">Use TLS</string>\n    <string name=\"sni\">Server Name Indication</string>\n    <string name=\"alpn\">Application-Layer Protocol Negotiation</string>\n    <string name=\"certificates\">Certificates</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Pinned Certificate Chain</string>\n    <string name=\"encryption\">Encryption</string>\n    <string name=\"extra_headers\">Extra Headers</string>\n    <string name=\"config_type\">Config Type</string>\n    <string name=\"edit_config\">Edit Config</string>\n    <!-- feature category -->\n    <string name=\"ipv6\">IPv6 Route</string>\n    <string name=\"prefer\">Prefer</string>\n    <string name=\"only\">Only</string>\n    <string name=\"metered\">Metered Hint</string>\n    <string name=\"metered_summary\">Hint system to treat VPN as metered</string>\n    <string name=\"menu_route\">Route</string>\n    <string name=\"route_add\">Create Route</string>\n    <string name=\"delete_route_prompt\">Are you sure you want to remove this route?</string>\n    <string name=\"route_name\">Route Name</string>\n    <string name=\"route_proxy\">Proxy</string>\n    <string name=\"route_bypass\">Bypass</string>\n    <string name=\"route_block\">Block</string>\n    <string name=\"route_profile\">Select Profile…</string>\n    <string name=\"empty_route\">Empty Route</string>\n    <string name=\"empty_route_notice\">Set some rules before saving</string>\n    <string name=\"route_bypass_domain\">Domain rule for %s</string>\n    <string name=\"route_bypass_ip\">IP rule for %s</string>\n    <string name=\"route_reset\">Reset</string>\n    <string name=\"route_reverse\">Reverse Proxy</string>\n    <string name=\"route_reverse_redirect\">Reverse Redirect</string>\n    <string name=\"route_manage_assets\">Manage Route Assets</string>\n    <string name=\"route_assets\">Assets</string>\n    <string name=\"route_rules_provider\">Rule Assets Provider</string>\n    <string name=\"route_rules_official\">Official</string>\n    <string name=\"route_asset_status\">Local version: %s</string>\n    <string name=\"route_asset_no_update\">No update</string>\n    <string name=\"route_asset_updated\">Updated</string>\n    <string name=\"route_not_asset\">Not an asset file: excepted .dat, but %s</string>\n    <string name=\"route_opt_bypass_lan\">Bypass LAN</string>\n    <string name=\"route_opt_block_ads\">Block ADs</string>\n    <string name=\"domain_strategy\">Domain Resolution Strategy</string>\n    <string name=\"traffic_sniffing\">Enable Traffic Sniffing</string>\n    <string name=\"enable_mux\">Enable Multiplexer</string>\n    <string name=\"mux_sum\">Mux is designed to reduce TCP handshake latency, not to increase connection throughput. Using Mux to watch videos, download or speed test is usually counter productive</string>\n    <string name=\"mux_concurrency\">Mux Concurrent Connections</string>\n    <string name=\"mux_for_all\">Enable Mux for all possible supported protocols</string>\n    <string name=\"mux_for_all_sum\">If the server does not support Mux, we will not be able to connect to it.</string>\n    <string name=\"tcp_keep_alive_interval\">TCP keep active packet delivery interval</string>\n    <string name=\"proxied_apps\">Apps VPN mode</string>\n    <string name=\"proxied_apps_summary\">Configure VPN mode for selected apps</string>\n    <string name=\"on\">On</string>\n    <string name=\"off\">Off</string>\n    <string name=\"search_apps\">Search…</string>\n    <string name=\"scanning\">Scanning…</string>\n    <string name=\"invert_selections\">Invert selections</string>\n    <string name=\"clear_selections\">Clear selections</string>\n    <string name=\"bypass_apps\">Bypass</string>\n    <string name=\"show_system_apps\">Show system apps</string>\n    <string name=\"auto_connect\">Auto Connect</string>\n    <string name=\"auto_connect_summary\">Enable proxy on startup/app update if it was running before</string>\n    <string name=\"direct_boot_aware\">Allow Toggling in Lock Screen</string>\n    <string name=\"direct_boot_aware_summary\">Your selected profile information will be less protected</string>\n    <!-- notification category -->\n    <string name=\"service_vpn\">VPN Service</string>\n    <string name=\"service_proxy\">Proxy Service</string>\n    <string name=\"forward_success\">Proxy started.</string>\n    <string name=\"invalid_server\">Invalid server name</string>\n    <string name=\"service_failed\">Failed: </string>\n    <string name=\"stop\">Stop</string>\n    <string name=\"stopping\">Shutting down…</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"vpn_permission_denied\">Permission denied to create a VPN service</string>\n    <string name=\"reboot_required\">Failed to start VPN service. You might need to reboot your device.</string>\n    <!-- alert category -->\n    <string name=\"profile_empty\">Please select a profile</string>\n    <string name=\"connect\">Connect</string>\n    <string name=\"clipboard_empty\">Clipboard is empty</string>\n    <!-- menu category -->\n    <string name=\"settings\">Settings</string>\n    <string name=\"edit\">Edit</string>\n    <string name=\"share\">Share</string>\n    <string name=\"add_profile\">Add Profile</string>\n    <string name=\"select_profile\">Select Profile</string>\n    <string name=\"action_socks\" translatable=\"false\">SOCKS</string>\n    <string name=\"action_http\" translatable=\"false\">HTTP</string>\n    <string name=\"action_shadowsocks\" translatable=\"false\">Shadowsocks</string>\n    <string name=\"action_shadowsocksr\" translatable=\"false\">ShadowsocksR</string>\n    <string name=\"action_vmess\" translatable=\"false\">VMess</string>\n    <string name=\"action_vless\" translatable=\"false\">VLESS</string>\n    <string name=\"action_trojan\" translatable=\"false\">Trojan</string>\n    <string name=\"action_trojan_go\" translatable=\"false\">Trojan Go</string>\n    <string name=\"action_naive\" translatable=\"false\">Naïve</string>\n    <string name=\"action_ping_tunnel\" translatable=\"false\">Ping Tunnel</string>\n    <string name=\"action_relay_baton\" translatable=\"false\">RelayBaton</string>\n    <string name=\"action_brook\" translatable=\"false\">Brook</string>\n    <string name=\"action_hysteria\" translatable=\"false\">Hysteria</string>\n    <string name=\"action_snell\" translatable=\"false\">Snell</string>\n    <string name=\"action_ssh\" translatable=\"false\">SSH</string>\n    <string name=\"action_wireguard\" translatable=\"false\">WireGuard</string>\n    <string name=\"proxy_chain\">Proxy Chain</string>\n    <string name=\"custom_config\">Custom Config</string>\n    <string name=\"balancer\">Balancer</string>\n    <string name=\"balancer_settings\">Balancer Settings</string>\n    <string name=\"balancer_type\">Type</string>\n    <string name=\"balancer_strategy\">Strategy</string>\n    <string name=\"chain_settings\">Chain Settings</string>\n    <string name=\"config_settings\">Config Settings</string>\n    <string name=\"circular_reference\">Circular reference</string>\n    <string name=\"circular_reference_sum\">The route cannot contain itself.</string>\n    <string name=\"clear_profiles\">Clear</string>\n    <string name=\"action_create_group\">Empty group</string>\n    <string name=\"action_from_link\">From subscription</string>\n    <string name=\"action_scan_china_apps\">Scan China apps</string>\n    <string name=\"action_export_file\">Export to file</string>\n    <string name=\"action_export_clipboard\">Export to Clipboard</string>\n    <string name=\"action_export\">Export</string>\n    <string name=\"action_import\">Import from Clipboard</string>\n    <string name=\"action_import_file\">Import from file</string>\n    <string name=\"action_export_msg\">Successfully export!</string>\n    <string name=\"action_export_err\">Failed to export.</string>\n    <string name=\"action_import_msg\">Successfully import!</string>\n    <string name=\"action_import_err\">Failed to import.</string>\n    <string name=\"file_manager_missing\">Your device lacks an Android standard file selector, please install one, such as Material Files.</string>\n    <!-- share -->\n    <!-- profile -->\n    <string name=\"profile_config\">Profile config</string>\n    <string name=\"delete\">Remove</string>\n    <string name=\"delete_confirm_prompt\">Are you sure you want to remove this profile?</string>\n    <string name=\"share_qr_nfc\">QR code</string>\n    <string name=\"add_profile_methods_scan_qr_code\">Scan QR code</string>\n    <string name=\"add_profile_methods_manual_settings\">Manual Settings</string>\n    <plurals name=\"removed\">\n        <item quantity=\"one\">Removed</item>\n        <item quantity=\"other\">%d items removed</item>\n    </plurals>\n    <plurals name=\"added\">\n        <item quantity=\"one\">Added</item>\n        <item quantity=\"other\">%d proxies added</item>\n    </plurals>\n    <string name=\"undo\">Undo</string>\n    <string name=\"insecure_warn\">Security Advisory</string>\n    <string name=\"insecure_warn_sum\">Provided by the Qv2ray Developer Community</string>\n    <string name=\"insecure\">Insecure</string>\n    <string name=\"deprecated\">Deprecated</string>\n    <!-- tasker -->\n    <!-- status -->\n    <string name=\"connecting\">Connecting…</string>\n    <string name=\"vpn_connected\">Connected, tap to check connection</string>\n    <string name=\"not_connected\">Not connected</string>\n    <!-- subscriptions -->\n    <string name=\"subscriptions\">Subscriptions</string>\n    <string name=\"cleartext_http_warning\">Cleartext HTTP traffic is insecure</string>\n    <string name=\"error_title\">Error</string>\n    <string name=\"no_proxies_found\">No proxies found in the link</string>\n    <string name=\"no_proxies_found_in_subscription\">No proxies found in the subscription</string>\n    <string name=\"no_proxies_found_in_file\">No proxies found in the file</string>\n    <string name=\"no_proxies_found_in_clipboard\">No proxies found in the clipboard</string>\n    <string name=\"deduplication\">Deduplication</string>\n    <string name=\"donate\">Donate</string>\n    <string name=\"donate_info\">I love money</string>\n    <string name=\"ignore_battery_optimizations\">Ignore battery optimizations</string>\n    <string name=\"ignore_battery_optimizations_sum\">Remove some restrictions</string>\n    <!-- plugin -->\n    <string name=\"plugin\">Plugin</string>\n    <string name=\"plugin_configure\">Configure…</string>\n    <string name=\"plugin_disabled\">Disabled</string>\n    <string name=\"plugin_unknown\">Unknown plugin %s</string>\n    <string name=\"plugin_untrusted\">Warning: This plugin does not seem to come from a known trusted source.</string>\n    <string name=\"plugin_auto_connect_unlock_only\">This plugin might not work with Auto Connect</string>\n    <string name=\"proxy_cat\">Server Settings</string>\n    <string name=\"ss_cat\">Shadowsocks Settings</string>\n    <string name=\"unsaved_changes_prompt\">Changes not saved. Do you want to save?</string>\n    <string name=\"yes\">Yes</string>\n    <string name=\"no\">No</string>\n    <string name=\"apply\">Apply</string>\n    <string name=\"restart\">Reload proxy service to apply changes</string>\n    <string name=\"license\">License</string>\n    <string name=\"route_warn\">Make sure you have read the documentation before adding custom rules, otherwise you may not be able to connect to the Internet.</string>\n    <string name=\"lines\">%d Lines</string>\n    <string name=\"night_mode\">Night Mode</string>\n    <string name=\"follow_system\">Follow System</string>\n    <string name=\"enable\">Enable</string>\n    <string name=\"disable\">Disable</string>\n    <string name=\"auto\">Auto</string>\n    <string name=\"enable_log\">Enable Log</string>\n    <string name=\"enable_log_sum\">For debugging purposes</string>\n    <string name=\"list\">List</string>\n    <string name=\"random\">Random</string>\n    <string name=\"leastPing\" translatable=\"false\">Ping</string>\n    <string name=\"api_port\">API Port</string>\n    <string name=\"probe_interval\">Balancer observation interval</string>\n    <string name=\"standard\">Standard</string>\n    <string name=\"v2rayn\" translatable=\"false\">V2RayN</string>\n    <string name=\"available\" translatable=\"false\">%dms</string>\n    <string name=\"unavailable\">Unavailable</string>\n    <string name=\"always_show_address\">Always Show Address</string>\n    <string name=\"always_show_address_sum\">Always display the server address on the configuration card</string>\n    <string name=\"clear_traffic_statistics\">Clear traffic statistics</string>\n    <string name=\"connection_test\">Connection test</string>\n    <string name=\"connection_test_clear_results\">Clear test results</string>\n    <string name=\"connection_test_tcp_ping\" translatable=\"false\">TCPing</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing unavailable</string>\n    <string name=\"connection_test_icmp_ping\" translatable=\"false\">ICMPing</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing unavailable</string>\n    <string name=\"connection_test_url_test\" translatable=\"false\">URL Test</string>\n    <string name=\"connection_test_domain_not_found\">Domain not found</string>\n    <string name=\"connection_test_refused\">Connection refused</string>\n    <string name=\"connection_test_unreachable\">Unreachable</string>\n    <string name=\"connection_test_timeout\">Timeout</string>\n    <string name=\"append_http_proxy\">Append HTTP Proxy to VPN</string>\n    <string name=\"append_http_proxy_sum\">HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+)</string>\n    <string name=\"bypass_lan_in_core_only\">Bypass LAN in Core Only</string>\n    <string name=\"bypass_lan_in_core_only_sum\">If Lineage\\'s \"Allow hotspot clients to use VPNs\" does not work, try this.</string>\n    <string name=\"protocol_settings\">Protocol Settings</string>\n    <string name=\"trojan_provider\">Trojan Provider</string>\n    <string name=\"ss_aead_provider\">Shadowsocks AEAD Provider</string>\n    <string name=\"ss_stream_provider\">Shadowsocks Stream Provider</string>\n    <string name=\"group_basic\">Basic</string>\n    <string name=\"group_settings\">Group Settings</string>\n    <string name=\"subscription\">Subscription</string>\n    <string name=\"subscription_settings\">Subscription Settings</string>\n    <string name=\"group_type\">Group Type</string>\n    <string name=\"subscription_type\">Subscription Type</string>\n    <string name=\"delete_group_prompt\">Are you sure you want to remove this group?</string>\n    <string name=\"force_resolve\">Force Resolve</string>\n    <string name=\"force_resolve_sum\">Resolve all domain names to IP addresses when updating. Host and SNI will be automatically appended if possible</string>\n    <string name=\"force_vmess_aead\">Force VMess AEAD</string>\n    <string name=\"force_vmess_aead_sum\">Rewrite VMess configuration with insecure authentication method to AEAD (alterId 0)</string>\n    <string name=\"deduplication_sum\">Remove duplicate configurations when updating</string>\n    <string name=\"raw\">Raw</string>\n    <string name=\"update_settings\">Update Settings</string>\n    <string name=\"auto_update\">Auto Update</string>\n    <string name=\"auto_update_delay\">Auto Update Delay (In minutes)</string>\n    <string name=\"update_when_connected_only\">Update only when connected</string>\n    <string name=\"update_when_connected_only_sum\">Prevent IP address leakage</string>\n    <string name=\"subscription_user_agent\">UserAgent</string>\n    <string name=\"confirm\">Confirm</string>\n    <string name=\"missing_plugin\">Missing Plugin</string>\n    <string name=\"profile_requiring_plugin\">Profile %s requires the %s plugin to be installed, but it was not found.</string>\n    <string name=\"action_learn_more\">LEARN MORE</string>\n    <string name=\"action_download\">DOWNLOAD</string>\n    <string name=\"install_from_play_store\">Install from Play Store</string>\n    <string name=\"install_from_fdroid\">Install from F-Droid</string>\n    <string name=\"download\">Download</string>\n    <string name=\"ooc_subscription_token\" translatable=\"false\">OOCv1 API Token</string>\n    <string name=\"ooc_subscription_token_invalid\">Invalid OOCv1 Token</string>\n    <string name=\"update_subscription_warning\">Proxy is not connected, are you sure you want to continue updating?</string>\n    <string name=\"ooc_warning\">Warning</string>\n    <string name=\"ooc_missing_protocol\">The subscription requires support for protocol %s, but it cannot be found. Unsupported profiles will be ignored.</string>\n    <string name=\"service_subscription\">Subscription Update Service</string>\n    <string name=\"subscription_update\">Subscription Update</string>\n    <string name=\"subscription_update_message\">Updating %s …</string>\n    <string name=\"group_filter\">Filter</string>\n    <string name=\"group_filter_groups\">Groups</string>\n    <string name=\"group_filter_tags\">Tags</string>\n    <string name=\"group_filter_owners\">Owners</string>\n    <string name=\"group_filter_ns\">OOCv1 subscription is required to use the filtering feature</string>\n    <string name=\"group_filter_groups_nf\">No groups in this subscription</string>\n    <string name=\"group_filter_tags_nf\">No tags in this subscription</string>\n    <string name=\"group_filter_owners_nf\">No owners in this subscription</string>\n    <string name=\"subscription_used\">%s Used</string>\n    <string name=\"subscription_traffic\">%s Used / %s Remaining</string>\n    <string name=\"subscription_import\">Import subscription</string>\n    <string name=\"subscription_import_message\">Confirm you want to import subscription %s? If you are coming from an untrusted source, doing this may result in your IP and this behavior being leaked.</string>\n    <string name=\"profile_import\">Import profile</string>\n    <string name=\"profile_import_message\">Confirm you want to import profile %s?</string>\n    <string name=\"clear_profiles_message\">Are you sure you want to clear this group?</string>\n    <string name=\"apps\">Applications</string>\n    <string name=\"select_apps\">Select Applications</string>\n    <string name=\"apps_message\">%d Applications</string>\n    <string name=\"hysteria_obfs\">Obfuscation Password</string>\n    <string name=\"hysteria_auth_type\">Authentication Type</string>\n    <string name=\"hysteria_auth_payload\">Authentication Payload</string>\n    <string name=\"hysteria_upload_mbps\">Max Upload Speed (in Mbps)</string>\n    <string name=\"hysteria_download_mbps\">Max Download Speed (in Mbps)</string>\n    <string name=\"experimental_settings\">Experimental</string>\n    <string name=\"experimental_authenticated_length\">Make the length of each payload segment no longer malleable. This experiment requires the server and client use the same version of v2ray-core. More breaking updates on this experiment is expected.</string>\n    <string name=\"experimental_no_termination_signal\">Do not send connection single duplex termination signal for TCP connection when transferred over VMess. This will break some application.</string>\n    <string name=\"utls_fingerprint\">uTLS Fingerprint</string>\n    <string name=\"hysteria_stream_receive_window\">QUIC Stream Receive Window</string>\n    <string name=\"hysteria_connection_receive_window\">QUIC Connection Receive Window</string>\n    <string name=\"hysteria_disable_mtu_discovery\">Disable Path MTU Discovery</string>\n    <string name=\"group_order\">Order</string>\n    <string name=\"group_order_origin\">Origin</string>\n    <string name=\"group_order_by_name\">By Name</string>\n    <string name=\"group_order_by_delay\">By Delay</string>\n    <string name=\"plugin_exists_but_on_shit_system\">Profile %s requires the %s plugin, but your proprietary equipment vendor (usually surveillance capital giants and malware maker) tampered with your Android, making the plugin unusable.</string>\n\n    <string name=\"shadowsocks_plugin_simple_obfs\">Simple Obfs (Shadowsocks Android Plugin)</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (Shadowsocks Android Plugin)</string>\n\n    <string name=\"traffic_uplink\" translatable=\"false\">%s | %s/s ↑</string>\n    <string name=\"traffic_downlink\" translatable=\"false\">%s | %s/s ↓</string>\n\n    <string name=\"traffic_uplink_total\" translatable=\"false\">%s ↑</string>\n    <string name=\"traffic_downlink_total\" translatable=\"false\">%s ↓</string>\n    <string name=\"traffic_active\">Active</string>\n    <string name=\"traffic_stats\">Statistics</string>\n\n    <string name=\"tcp_connections\">%d TCP connections</string>\n    <string name=\"udp_connections\">%d UDP connections</string>\n\n    <string name=\"traffic_holder\">Open VPN to record traffic statistics</string>\n\n    <string name=\"copy_label\">Copy Name</string>\n    <string name=\"copy_package_name\">Copy Package Name</string>\n    <string name=\"open_app\">Open App</string>\n    <string name=\"open_settings\">Open Settings</string>\n    <string name=\"open_market\">Open Market</string>\n    <string name=\"create_rule\">Create Rule</string>\n\n    <string name=\"copy_success\">Copy success!</string>\n    <string name=\"copy_failed\">Copy failed.</string>\n    <string name=\"app_no_launcher\">The app has no interface.</string>\n    <string name=\"route_for\">Rule for %s</string>\n\n    <string name=\"foreground_detector\">Foreground detector</string>\n    <string name=\"foreground_detector_summary\">This service provides support for routing rules based on the foreground state of apps. If it is not started, the result is always in the background.</string>\n\n    <string name=\"foreground_status\">Foreground Status</string>\n    <string name=\"foreground\">Foreground</string>\n    <string name=\"background\">Background</string>\n\n    <string name=\"route_need_vpn\">Routing rule %s relies on the VPN to be in effect, so it is ignored.</string>\n    <string name=\"route_need_fds\">Routing rule %s relies on the Foreground Detector Service to be in effect, but is not enabled.</string>\n\n    <string name=\"app_traffic_statistics\">App Traffic Statistics</string>\n    <string name=\"profile_traffic_statistics\">Profile Traffic Statistics</string>\n    <string name=\"traffic_statistics_summary\">May increase power consumption</string>\n    <string name=\"profile_traffic_statistics_summary\">When disabled, the used traffic will not be counted</string>\n    <string name=\"no_statistics\">No statistics yet</string>\n    <string name=\"app_statistics_disabled\">App Traffic statistics disabled</string>\n    <string name=\"ssh_auth_type_none\">None</string>\n    <string name=\"ssh_public_key\">Public Key</string>\n    <string name=\"ssh_private_key\">Private Key</string>\n    <string name=\"ssh_private_key_passphrase\">Private Key Passphrase</string>\n    <string name=\"translate_platform\">Translate Platform</string>\n    <string name=\"menu_tools\">Tools</string>\n    <string name=\"menu_log\">Logs</string>\n    <string name=\"clear_logcat\">Clear Logcat</string>\n    <string name=\"wireguard_local_address\">Local Address</string>\n    <string name=\"wireguard_public_key\">Peer Public Key</string>\n    <string name=\"wireguard_psk\">Peer Pre-Shared Key</string>\n\n    <string name=\"cloudflare_wrap\" translatable=\"false\">Warp</string>\n    <string name=\"warp_license\">CloudFlare Wrap is a free WireGuard VPN provider. By using it, you agree to the TOS: https://www.cloudflare.com/application/terms/</string>\n    <string name=\"warp_generate\">Generate Configuration</string>\n    <string name=\"generating\">Generating…</string>\n    <string name=\"tun_implementation\">TUN Implementation</string>\n    <string name=\"destination_override\">Override Destination</string>\n    <string name=\"destination_override_summary\">Use the sniffed domain to overwrite the destination address, not just for routing</string>\n    <string name=\"resolve_destination\">Resolve Destination</string>\n    <string name=\"resolve_destination_summary\">If the destination address is a domain, it is then passed out based on the IPv6 strategy.</string>\n    <string name=\"pcap\" translatable=\"false\">Pcap</string>\n    <string name=\"enable_pcap\">Enable Pcap</string>\n    <string name=\"pcap_summary\">Save traffic to .pcap file</string>\n    <string name=\"pcap_notice\">Pcap files will be saved to %s</string>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.SagerNet\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <item name=\"actionBarStyle\">@style/Widget.MaterialComponents.ActionBar.Solid</item>\n        <item name=\"actionModeCloseDrawable\">@drawable/ic_navigation_close</item>\n        <item name=\"colorButtonNormal\">?colorAccent</item>\n        <item name=\"android:navigationBarColor\">?colorPrimaryDark</item>\n        <item name=\"colorPrimary\">@color/material_pink_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_pink_700</item>\n        <item name=\"colorAccent\">@color/material_pink_accent_200</item>\n\n        <item name=\"colorMaterial100\">@color/material_pink_100</item>\n        <item name=\"colorMaterial300\">@color/material_pink_300</item>\n\n        <item name=\"accentOrTextSecondary\">?colorAccent</item>\n        <item name=\"accentOrTextPrimary\">?colorAccent</item>\n        <item name=\"primaryOrTextSecondary\">?colorPrimary</item>\n        <item name=\"primaryOrTextPrimary\">?colorPrimary</item>\n\n        <item name=\"fabColorBackground\">?colorPrimary</item>\n        <item name=\"selectedColorPrimary\">?colorPrimary</item>\n        <item name=\"actionBarTheme\">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>\n        <item name=\"tabTextColor\">#99FFFFFF</item>\n        <item name=\"tabSelectedTextColor\">@color/white</item>\n        <item name=\"tabIndicatorColor\">@color/white</item>\n        <item name=\"tabRippleColor\">@color/white</item>\n        <item name=\"whiteOrTextPrimary\">@color/white</item>\n\n        <item name=\"windowActionModeOverlay\">true</item>\n\n        <!-- Remove ActionBar but keep styles and themes -->\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n\n        <item name=\"chipStyle\">@style/Widget.MaterialComponents.Chip.Choice.SagerNet</item>\n\n        <item name=\"itemShapeAppearance\">\n            @style/ShapeAppearance.MaterialComponents.MediumComponent\n        </item>\n\n        <item name=\"alertDialogTheme\">\n            @style/ThemeOverlay.MaterialComponents.MaterialAlertDialog.SagerNet\n        </item>\n\n        <item name=\"materialAlertDialogTheme\">\n            @style/ThemeOverlay.MaterialComponents.MaterialAlertDialog.SagerNet\n        </item>\n\n    </style>\n\n    <style name=\"ThemeOverlay.MaterialComponents.MaterialAlertDialog.SagerNet\">\n        <item name=\"buttonBarPositiveButtonStyle\">\n            @style/Widget.MaterialComponents.Button.TextButton.Dialog.Primary\n        </item>\n        <item name=\"buttonBarNegativeButtonStyle\">\n            @style/Widget.MaterialComponents.Button.TextButton.Dialog.Primary\n        </item>\n        <item name=\"buttonBarNeutralButtonStyle\">\n            @style/Widget.MaterialComponents.Button.TextButton.Dialog.Flush.Primary\n        </item>\n    </style>\n\n    <style name=\"Widget.MaterialComponents.Button.TextButton.Dialog.Primary\">\n        <item name=\"android:textColor\">?primaryOrTextPrimary</item>\n    </style>\n\n    <style name=\"Widget.MaterialComponents.Button.TextButton.Dialog.Flush.Primary\">\n        <item name=\"android:textColor\">?primaryOrTextPrimary</item>\n    </style>\n\n    <style name=\"Widget.MaterialComponents.Chip.Choice.SagerNet\">\n        <item name=\"textStartPadding\">30dp</item>\n        <item name=\"textEndPadding\">30dp</item>\n        <item name=\"chipBackgroundColor\">@color/chip_background</item>\n        <item name=\"android:textColor\">@color/chip_text_color</item>\n        <item name=\"checkedIconTint\">?primaryOrTextPrimary</item>\n        <item name=\"rippleColor\">@color/chip_ripple_color</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Black\">\n        <item name=\"colorPrimary\">@color/material_light_black</item>\n        <item name=\"colorPrimaryDark\">@color/black</item>\n        <item name=\"colorAccent\">@color/white</item>\n        <item name=\"colorMaterial100\">@color/fab_color_progress</item>\n        <item name=\"colorMaterial300\">@color/material_light_black</item>\n\n        <item name=\"pref_categoryColor\">?android:textColorPrimary</item>\n        <item name=\"android:textColorLink\">?attr/accentOrTextSecondary</item>\n        <item name=\"fabColorBackground\">?colorPrimaryDark</item>\n        <item name=\"selectedColorPrimary\">@color/white</item>\n        <item name=\"itemShapeFillColor\">#616161</item>\n\n        <item name=\"accentOrTextSecondary\">?android:textColorSecondary</item>\n        <item name=\"accentOrTextPrimary\">?android:textColorPrimary</item>\n        <item name=\"primaryOrTextSecondary\">?android:textColorSecondary</item>\n        <item name=\"primaryOrTextPrimary\">?android:textColorPrimary</item>\n    </style>\n\n\n    <style name=\"Theme.SagerNet.LightBlack\">\n        <item name=\"colorPrimary\">@color/white</item>\n        <item name=\"colorPrimaryDark\">@color/material_light_white</item>\n        <item name=\"colorAccent\">@color/black</item>\n        <item name=\"colorMaterial300\">@color/white</item>\n\n        <item name=\"actionBarTheme\">@style/ThemeOverlay.AppCompat.ActionBar</item>\n        <item name=\"selectedColorPrimary\">@color/black</item>\n        <item name=\"whiteOrTextPrimary\">?android:textColorPrimary</item>\n        <item name=\"fabColorBackground\">@color/white</item>\n\n        <item name=\"tabTextColor\">?android:textColorSecondary</item>\n        <item name=\"tabSelectedTextColor\">?android:textColorPrimary</item>\n        <item name=\"tabIndicatorColor\">@color/black</item>\n        <item name=\"tabRippleColor\">@android:color/transparent</item>\n\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true</item>\n        <item name=\"itemShapeFillColor\">#E0E0E0</item>\n\n        <item name=\"colorMaterial100\">@color/fab_color_progress</item>\n        <item name=\"pref_categoryColor\">?android:textColorPrimary</item>\n        <item name=\"android:textColorLink\">?attr/accentOrTextSecondary</item>\n\n        <item name=\"accentOrTextSecondary\">?android:textColorSecondary</item>\n        <item name=\"accentOrTextPrimary\">?android:textColorPrimary</item>\n        <item name=\"primaryOrTextSecondary\">?android:textColorSecondary</item>\n        <item name=\"primaryOrTextPrimary\">?android:textColorPrimary</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Amber\">\n        <item name=\"colorPrimary\">@color/material_amber_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_amber_700</item>\n        <item name=\"colorAccent\">@color/material_amber_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_amber_100</item>\n        <item name=\"colorMaterial300\">@color/material_amber_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Blue\">\n        <item name=\"colorPrimary\">@color/material_blue_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_blue_700</item>\n        <item name=\"colorAccent\">@color/material_blue_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_blue_100</item>\n        <item name=\"colorMaterial300\">@color/material_blue_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.BlueGrey\">\n        <item name=\"colorPrimary\">@color/material_blue_grey_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_blue_grey_700</item>\n        <item name=\"colorAccent\">@color/material_blue_grey_200</item>\n        <item name=\"colorMaterial100\">@color/material_blue_grey_100</item>\n        <item name=\"colorMaterial300\">@color/material_blue_grey_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Brown\">\n        <item name=\"colorPrimary\">@color/material_brown_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_brown_700</item>\n        <item name=\"colorAccent\">@color/material_brown_200</item>\n        <item name=\"colorMaterial100\">@color/material_brown_100</item>\n        <item name=\"colorMaterial300\">@color/material_brown_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Cyan\">\n        <item name=\"colorPrimary\">@color/material_cyan_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_cyan_700</item>\n        <item name=\"colorAccent\">@color/material_cyan_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_cyan_100</item>\n        <item name=\"colorMaterial300\">@color/material_cyan_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.DeepOrange\">\n        <item name=\"colorPrimary\">@color/material_deep_orange_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_deep_orange_700</item>\n        <item name=\"colorAccent\">@color/material_deep_orange_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_deep_orange_100</item>\n        <item name=\"colorMaterial300\">@color/material_deep_orange_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.DeepPurple\">\n        <item name=\"colorPrimary\">@color/material_deep_purple_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_deep_purple_700</item>\n        <item name=\"colorAccent\">@color/material_deep_purple_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_deep_purple_100</item>\n        <item name=\"colorMaterial300\">@color/material_deep_purple_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Green\">\n        <item name=\"colorPrimary\">@color/material_green_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_green_700</item>\n        <item name=\"colorAccent\">@color/material_green_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_green_100</item>\n        <item name=\"colorMaterial300\">@color/material_green_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Grey\">\n        <item name=\"colorPrimary\">@color/material_grey_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_grey_700</item>\n        <item name=\"colorAccent\">@color/material_grey_200</item>\n        <item name=\"colorMaterial100\">@color/material_grey_100</item>\n        <item name=\"colorMaterial300\">@color/material_grey_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Indigo\">\n        <item name=\"colorPrimary\">@color/material_indigo_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_indigo_700</item>\n        <item name=\"colorAccent\">@color/material_indigo_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_indigo_100</item>\n        <item name=\"colorMaterial300\">@color/material_indigo_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.LightBlue\">\n        <item name=\"colorPrimary\">@color/material_light_blue_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_light_blue_700</item>\n        <item name=\"colorAccent\">@color/material_light_blue_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_light_blue_100</item>\n        <item name=\"colorMaterial300\">@color/material_light_blue_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.LightGreen\">\n        <item name=\"colorPrimary\">@color/material_light_green_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_light_green_700</item>\n        <item name=\"colorAccent\">@color/material_light_green_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_light_green_100</item>\n        <item name=\"colorMaterial300\">@color/material_light_green_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Lime\">\n        <item name=\"colorPrimary\">@color/material_lime_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_lime_700</item>\n        <item name=\"colorAccent\">@color/material_lime_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_lime_100</item>\n        <item name=\"colorMaterial300\">@color/material_lime_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Orange\">\n        <item name=\"colorPrimary\">@color/material_orange_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_orange_700</item>\n        <item name=\"colorAccent\">@color/material_orange_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_orange_100</item>\n        <item name=\"colorMaterial300\">@color/material_orange_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Purple\">\n        <item name=\"colorPrimary\">@color/material_purple_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_purple_700</item>\n        <item name=\"colorAccent\">@color/material_purple_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_purple_100</item>\n        <item name=\"colorMaterial300\">@color/material_purple_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Red\">\n        <item name=\"colorPrimary\">@color/material_red_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_red_700</item>\n        <item name=\"colorAccent\">@color/material_red_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_red_100</item>\n        <item name=\"colorMaterial300\">@color/material_red_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Teal\">\n        <item name=\"colorPrimary\">@color/material_teal_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_teal_700</item>\n        <item name=\"colorAccent\">@color/material_teal_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_teal_100</item>\n        <item name=\"colorMaterial300\">@color/material_teal_300</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Yellow\">\n        <item name=\"colorPrimary\">@color/material_yellow_500</item>\n        <item name=\"colorPrimaryDark\">@color/material_yellow_700</item>\n        <item name=\"colorAccent\">@color/material_yellow_accent_200</item>\n        <item name=\"colorMaterial100\">@color/material_yellow_100</item>\n        <item name=\"colorMaterial300\">@color/material_yellow_300</item>\n        <item name=\"colorOnPrimarySurface\">@color/black</item>\n    </style>\n\n    <style name=\"Theme.SagerNet.Start\" parent=\"Theme.MaterialComponents.Light.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n        <item name=\"colorPrimary\">@android:color/transparent</item>\n        <item name=\"colorPrimaryDark\">@android:color/transparent</item>\n        <item name=\"android:backgroundDimEnabled\">false</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"menu_group\">مجموعة</string>\n    <string name=\"menu_configuration\">ترتيب</string>\n    <string name=\"logcat_summary\">هذا مفيد جدا نجاح باهر</string>\n    <string name=\"logcat\">تصدير معلومات التصحيح</string>\n    <string name=\"version_x\">الإصدار (%s)</string>\n    <string name=\"app_version\">إصدار</string>\n    <string name=\"oss_licenses\">تراخيص مفتوحة المصدر</string>\n    <string name=\"telegram\">قناة تحديث Telegram</string>\n    <string name=\"github\">مصدر الرمز</string>\n    <string name=\"app_desc\">سلسلة أدوات البروکسی العالمية لنظام Android ، مكتوبة بلغة Kotlin.</string>\n    <string name=\"project\">مشروع</string>\n    <string name=\"group_status_proxies\">%d البروکسیز</string>\n    <string name=\"group_status_empty_subscription\">لم يتم تحديثه بعد</string>\n    <string name=\"group_status_empty\">مجموعة فارغة</string>\n    <string name=\"group_edit_subscription\">تحرير الاشتراك‬</string>\n    <string name=\"group_edit\">تحرير المجموعة</string>\n    <string name=\"group_create_subscription\">إنشاء اشتراك</string>\n    <string name=\"group_create\">إنشاء مجموعة</string>\n    <string name=\"group_name_required\">اسم المجموعة مطلوب</string>\n    <string name=\"group_subscription_link\">رابط الاشتراك</string>\n    <string name=\"group_update\">تحديث</string>\n    <string name=\"group_name\">أسم المجموعة</string>\n    <string name=\"tile_title\">الجلاد</string>\n    <string name=\"quick_toggle\">تبديل</string>\n    <string name=\"group_default\">غير مجمعة</string>\n    <string name=\"document\">وثيقة</string>\n    <string name=\"theme\">موضوع</string>\n    <string name=\"menu_about\">حول</string>\n    <string name=\"group_status_proxies_subscription\">%d البروکسیز | تم التحديث في %s</string>\n    <string name=\"group_updated\">%s: تم تحديث %d البروکسیز</string>\n    <string name=\"port_http\">منفذ بروکسی HTTP</string>\n    <string name=\"port_proxy\">منفذ بروکسی SOCKS5</string>\n    <string name=\"service_mode_proxy\">البروکسی فقط</string>\n    <string name=\"port_transproxy\">منفذ ترانسبروكسى</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode\">وضع الخدمة</string>\n    <string name=\"group_duplicate\">ينسخ:\n\\n%s</string>\n    <string name=\"group_deleted\">تم الحذف:\n\\n%s</string>\n    <string name=\"group_changed\">محدث:\n\\n%s</string>\n    <string name=\"group_added\">تمت الإضافة:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">هل أنت متأكد أنك تريد إزالة هذه المجموعة؟</string>\n    <string name=\"group_diff\">الفارق (%s)</string>\n    <string name=\"group_show_diff\">الفارق</string>\n    <string name=\"group_no_difference\">%s: لا فرق</string>\n    <string name=\"grpc_service_name\">اسم خدمة gRPC</string>\n    <string name=\"kcp_seed\">mKCP بذور(Seed)</string>\n    <string name=\"quic_key\">مفتاح Quic</string>\n    <string name=\"quic_security\">أمن QUIC</string>\n    <string name=\"http_path\">مسار HTTP</string>\n    <string name=\"http_host\">مضيف HTTP</string>\n    <string name=\"ws_path\">مسار WebSocket</string>\n    <string name=\"ws_host\">مضيف WebSocket</string>\n    <string name=\"header_type\">نوع الرأس</string>\n    <string name=\"network\">شبكة</string>\n    <string name=\"security\">تشفير طبقة النقل</string>\n    <string name=\"alter_id\">تغيير رقم التعريف</string>\n    <string name=\"uuid\">معرف المستخدم</string>\n    <string name=\"obfs_param\">Obfs بارام</string>\n    <string name=\"obfs\">Obfs(أو بی اِف اِس)</string>\n    <string name=\"protocol_param\">بروتوكول بارام</string>\n    <string name=\"protocol\">البروتوكول</string>\n    <string name=\"enc_method\">طريقة التشفير</string>\n    <string name=\"password\">كلمة المرور</string>\n    <string name=\"key_opt\">مفتاح (اختياري)</string>\n    <string name=\"password_opt\">كلمة المرور (اختياري)</string>\n    <string name=\"username_opt\">اسم مستخدِم (اختياري)</string>\n    <string name=\"username\">اسم مستخدِم</string>\n    <string name=\"server_port\">منفذ بعيد</string>\n    <string name=\"server_address\">الخادم</string>\n    <string name=\"profile_name\">اسم ملف التعريف</string>\n    <string name=\"connection_test_url\">اختبار الاتصال URL</string>\n    <string name=\"transproxy_mode\">وضع ترانسبروكسى</string>\n    <string name=\"require_transproxy\">تفعيل ترانسبروكسی الوارد</string>\n    <string name=\"port_local_dns\">منفذ DNS المحلي</string>\n    <string name=\"dns_hosts\">إعادة كتابة المجال</string>\n    <string name=\"fakedns_message\">قد يتسبب في الحاجة إلى إعادة تشغيل التطبيقات الأخرى لإعادة الاتصال بالشبكة بعد توقف البروکسی</string>\n    <string name=\"enable_fakedns\">تفعيل DNS وهمي</string>\n    <string name=\"dns_routing_message\">حل المجالات في مسارات الالتفاف باستخدام Direct DNS. كن على علم بتسريبات DNS المحتملة</string>\n    <string name=\"enable_dns_routing\">تمكين توجيه DNS</string>\n    <string name=\"direct_dns\">DNS المباشر</string>\n    <string name=\"remote_dns\">DNS البعيد</string>\n    <string name=\"cag_dns\">إعدادات DNS</string>\n    <string name=\"connection_test_error_status_code\">رمز الخطأ: #%d</string>\n    <string name=\"connection_test_fail\">الإنترنت غير متوفر</string>\n    <string name=\"connection_test_error\">فشل: %s</string>\n    <string name=\"connection_test_available_http\">نجاح: استغرقت مصافحة HTTP %dms</string>\n    <string name=\"connection_test_available\">نجاح: استغرقت مصافحة HTTPS %dms</string>\n    <string name=\"connection_test_testing\">اختبارات…</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"speed_detail\">بروکسی:\n\\n\\t%1$s↑ %2$s↓\n\\nمباشر:\n\\n\\t%3$s↑ %4$s↓</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"allow_insecure_sum\">تعطيل فحص الشهادة. عند التمكين ، يكون هذا التكوين آمنًا مثل النص العادي</string>\n    <string name=\"allow_insecure\">السماح بعدم الأمان</string>\n    <string name=\"security_settings\">اعدادات الامان</string>\n    <string name=\"show_direct_speed_sum\">أظهر سرعة المرور بدون وكيل في الإشعار أيضًا</string>\n    <string name=\"show_direct_speed\">عرض السرعة المباشرة</string>\n    <string name=\"show_stop_sum\">إذا كنت لا تريد استخدام \\\"اللوحة السريعة\\\" كمفتاح التبديل</string>\n    <string name=\"allow_access_sum\">ربط خوادم الإدخال إلى 0.0.0.0</string>\n    <string name=\"show_stop\">إظهار زر الإيقاف</string>\n    <string name=\"cag_misc\">إعدادات متنوعة</string>\n    <string name=\"speed_interval\">الفاصل الزمني لتحديث إشعار السرعة</string>\n    <string name=\"ws_browser_forwarding_sum\">إعادة توجيه WebSockets المقابلة من خلال المتصفح.</string>\n    <string name=\"ws_browser_forwarding\">إعادة توجيه المتصفح</string>\n    <string name=\"cag_ws\">إعدادات WebSocket</string>\n    <string name=\"require_http\">تفعيل HTTP الوارد</string>\n    <string name=\"general_settings\">إعدادات التطبيقات</string>\n    <string name=\"inbound_settings\">إعدادات الواردة</string>\n    <string name=\"allow_access\">السماح بالاتصالات من الشبكة المحلية</string>\n    <string name=\"cag_route\">إعدادات الطريق</string>\n    <string name=\"route_asset_updated\">محدث</string>\n    <string name=\"route_asset_no_update\">لا تحديث</string>\n    <string name=\"route_asset_status\">الإصدار المحلي: %s</string>\n    <string name=\"route_rules_official\">الرسمية</string>\n    <string name=\"route_rules_provider\">مزود أصول القاعدة</string>\n    <string name=\"route_assets\">الأصول</string>\n    <string name=\"route_manage_assets\">إدارة أصول الطريق</string>\n    <string name=\"route_reverse_redirect\">إعادة التوجيه العكسي</string>\n    <string name=\"route_reverse\">عكس البروکسی</string>\n    <string name=\"route_reset\">إعادة تعيين</string>\n    <string name=\"route_bypass_ip\">قاعدة IP ل %s</string>\n    <string name=\"route_bypass_domain\">قاعدة المجال ل %s</string>\n    <string name=\"empty_route_notice\">ضع بعض القواعد قبل الحفظ</string>\n    <string name=\"empty_route\">طريق فارغ</string>\n    <string name=\"route_profile\">حدد ملف التعريف…</string>\n    <string name=\"route_block\">منع</string>\n    <string name=\"route_bypass\">ضع جانبا</string>\n    <string name=\"route_proxy\">بروکسی</string>\n    <string name=\"route_name\">اسم الطريق</string>\n    <string name=\"delete_route_prompt\">هل أنت متأكد أنك تريد إزالة هذا المسار؟</string>\n    <string name=\"route_add\">إنشاء الطريق</string>\n    <string name=\"menu_route\">طريق</string>\n    <string name=\"metered_summary\">نظام تلميح لمعاملة VPN على أنها محسوبة</string>\n    <string name=\"metered\">تلميح مقنن</string>\n    <string name=\"only\">فقط</string>\n    <string name=\"prefer\">يفضل</string>\n    <string name=\"ipv6\">مسار IPv6</string>\n    <string name=\"edit_config\">تحرير التكوين</string>\n    <string name=\"config_type\">نوع التكوين</string>\n    <string name=\"extra_headers\">رؤوس إضافية</string>\n    <string name=\"encryption\">التشفير</string>\n    <string name=\"early_data_header_name\">اسم رأس البيانات المبكر</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">سلسلة الشهادات المثبتة</string>\n    <string name=\"certificates\">الشهادات</string>\n    <string name=\"alpn\">تفاوض بروتوكول طبقة التطبيق</string>\n    <string name=\"sni\">بيان اسم الخادم</string>\n    <string name=\"tls\">استخدم TLS</string>\n    <string name=\"add_profile_methods_scan_qr_code\">مسح رمز كيو آر</string>\n    <string name=\"share_qr_nfc\">رمز كيو آر</string>\n    <string name=\"delete_confirm_prompt\">هل أنت متأكد أنك تريد إزالة هذا ملف التعريف؟</string>\n    <string name=\"delete\">إزالة</string>\n    <string name=\"profile_config\">تكوين ملف التعريف</string>\n    <string name=\"file_manager_missing\">جهازك يفتقر إلى محدد ملفات أندرويد القياسي، يرجى تثبيت واحد، مثل Material Files.</string>\n    <string name=\"action_import_err\">فشل الاستيراد.</string>\n    <string name=\"action_import_msg\">تم الاستيراد بنجاح!</string>\n    <string name=\"action_export_err\">فشل التصدير.</string>\n    <string name=\"action_export_msg\">تم التصدير بنجاح!</string>\n    <string name=\"action_import_file\">استيراد من ملف</string>\n    <string name=\"action_import\">الاستيراد من الحافظة</string>\n    <string name=\"action_export\">يصدر</string>\n    <string name=\"action_export_clipboard\">تصدير إلى الحافظة</string>\n    <string name=\"action_export_file\">تصدير إلى ملف</string>\n    <string name=\"action_scan_china_apps\">فحص تطبيقات الصين</string>\n    <string name=\"action_from_link\">من الاشتراك</string>\n    <string name=\"action_create_group\">مجموعة فارغة</string>\n    <string name=\"clear_profiles\">صافي</string>\n    <string name=\"circular_reference_sum\">لا يمكن أن يحتوي المسار على نفسه.</string>\n    <string name=\"circular_reference\">مرجع دائري</string>\n    <string name=\"config_settings\">إعدادات التكوين</string>\n    <string name=\"chain_settings\">إعدادات السلسلة</string>\n    <string name=\"balancer_strategy\">إستراتيجية</string>\n    <string name=\"balancer_type\">اكتب</string>\n    <string name=\"balancer_settings\">إعدادات الموازن</string>\n    <string name=\"balancer\">موازن</string>\n    <string name=\"custom_config\">التكوين المخصص</string>\n    <string name=\"proxy_chain\">سلسلة البروکسی</string>\n    <string name=\"select_profile\">حدد ملف التعريف</string>\n    <string name=\"add_profile\">إضافة ملف تعريف</string>\n    <string name=\"share\">مشاركة</string>\n    <string name=\"edit\">تعديل</string>\n    <string name=\"settings\">إعدادات</string>\n    <string name=\"clipboard_empty\">الحافظة فارغة</string>\n    <string name=\"connect\">الاتصال</string>\n    <string name=\"profile_empty\">الرجاء تحديد ملف تعريف</string>\n    <string name=\"reboot_required\">فشل بدء تشغيل خدمة VPN. قد تحتاج إلى إعادة تشغيل جهازك.</string>\n    <string name=\"vpn_permission_denied\">تم رفض الإذن لإنشاء خدمة VPN</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"stopping\">جارٍ إيقاف التشغيل…</string>\n    <string name=\"stop\">قف</string>\n    <string name=\"service_failed\">باءت بالفشل:</string>\n    <string name=\"invalid_server\">اسم الخادم غير صالح</string>\n    <string name=\"forward_success\">بدأت البروکسی.</string>\n    <string name=\"service_proxy\">خدمة البروکسی</string>\n    <string name=\"service_vpn\">خدمة VPN</string>\n    <string name=\"direct_boot_aware_summary\">ستكون معلومات ملف التعريف المحدد أقل حماية</string>\n    <string name=\"direct_boot_aware\">السماح بالتبديل في شاشة القفل</string>\n    <string name=\"auto_connect_summary\">قم بتمكين البروکسی عند تحديث بدء التشغيل/التطبيق إذا كان يعمل من قبل</string>\n    <string name=\"auto_connect\">اتصال تلقائي</string>\n    <string name=\"show_system_apps\">عرض تطبيقات النظام</string>\n    <string name=\"bypass_apps\">ضع جانبا</string>\n    <string name=\"clear_selections\">تحديدات واضحة</string>\n    <string name=\"invert_selections\">عكس التحديدات</string>\n    <string name=\"scanning\">يتم المسح…</string>\n    <string name=\"search_apps\">بحث…</string>\n    <string name=\"off\">عن</string>\n    <string name=\"on\">على</string>\n    <string name=\"proxied_apps_summary\">تكوين وضع VPN للتطبيقات المحددة</string>\n    <string name=\"proxied_apps\">وضع تطبيقات VPN</string>\n    <string name=\"tcp_keep_alive_interval\">يحتفظ TCP بالفاصل الزمني النشط لتسليم الحزم</string>\n    <string name=\"mux_sum\">تم تصميم Mux لتقليل زمن انتقال مصافحة TCP، وليس لزيادة إنتاجية الاتصال. عادةً ما يؤدي استخدام Mux لمشاهدة مقاطع الفيديو أو التنزيل أو اختبار السرعة إلى نتائج عكسية</string>\n    <string name=\"mux_for_all_sum\">إذا كان الخادم لا يدعم Mux، فلن نتمكن من الاتصال به.</string>\n    <string name=\"mux_for_all\">قم بتمكين Mux لجميع البروتوكولات المدعومة الممكنة</string>\n    <string name=\"mux_concurrency\">اتصالات مسك المتزامنة</string>\n    <string name=\"enable_mux\">تمكين المضاعف</string>\n    <string name=\"traffic_sniffing\">تمكين استنشاق حركة المرور</string>\n    <string name=\"domain_strategy\">استراتيجية حل المجال</string>\n    <string name=\"route_opt_block_ads\">إعلانات كتلة</string>\n    <string name=\"route_opt_bypass_lan\">جانبا LAN</string>\n    <string name=\"route_not_asset\">ليس ملف أصل: استثناء .dat، لكن %s</string>\n    <string name=\"hysteria_download_mbps\">أقصى سرعة تنزيل (بالميجابت في الثانية)</string>\n    <string name=\"hysteria_upload_mbps\">أقصى سرعة تحميل (بالميجابت في الثانية)</string>\n    <string name=\"hysteria_auth_payload\">حمولة المصادقة</string>\n    <string name=\"hysteria_auth_type\">نوع المصادقة</string>\n    <string name=\"hysteria_obfs\">كلمة مرور التشويش</string>\n    <string name=\"apps_message\">%d تطبيقات</string>\n    <string name=\"select_apps\">حدد التطبيقات</string>\n    <string name=\"apps\">التطبيقات</string>\n    <string name=\"clear_profiles_message\">هل أنت متأكد أنك تريد مسح هذه المجموعة؟</string>\n    <string name=\"profile_import_message\">هل تريد تأكيد رغبتك في استيراد ملف التعريف %s؟</string>\n    <string name=\"profile_import\">استيراد ملف التعريف</string>\n    <string name=\"subscription_import_message\">هل تريد تأكيد رغبتك في استيراد اشتراك %s؟ إذا كنت قادمًا من مصدر غير موثوق به، فقد يؤدي القيام بذلك إلى تسريب عنوان IP الخاص بك وهذا السلوك.</string>\n    <string name=\"subscription_import\">اشتراك الاستيراد</string>\n    <string name=\"subscription_traffic\">%s مستخدمة / %s متبقية</string>\n    <string name=\"subscription_used\">%s مستخدمة</string>\n    <string name=\"group_filter_tags_nf\">لا توجد علامات في هذا الاشتراك</string>\n    <string name=\"group_filter_groups_nf\">لا توجد مجموعات في هذا الاشتراك</string>\n    <string name=\"group_filter_ns\">مطلوب اشتراك OOCv1 لاستخدام ميزة التصفية</string>\n    <string name=\"group_filter_tags\">العلامات</string>\n    <string name=\"group_filter_groups\">مجموعات</string>\n    <string name=\"group_filter\">التكرير</string>\n    <string name=\"subscription_update_message\">جاري تحديث %s …</string>\n    <string name=\"subscription_update\">تحديث الاشتراك</string>\n    <string name=\"service_subscription\">خدمة تحديث الاشتراك</string>\n    <string name=\"ooc_missing_protocol\">يتطلب الاشتراك دعم البروتوكول %s، ولكن لا يمكن العثور عليه. سيتم تجاهل الملفات الشخصية غير المدعومة.</string>\n    <string name=\"ooc_warning\">تحذير</string>\n    <string name=\"update_subscription_warning\">البروکسی غير متصل، هل أنت متأكد أنك تريد متابعة التحديث؟</string>\n    <string name=\"ooc_subscription_token_invalid\">رمز OOCv1 غير صالح</string>\n    <string name=\"download\">تحميل</string>\n    <string name=\"install_from_fdroid\">التثبيت من F-Droid</string>\n    <string name=\"install_from_play_store\">التثبيت من متجر Play</string>\n    <string name=\"action_download\">تحميل</string>\n    <string name=\"action_learn_more\">يتعلم أكثر</string>\n    <string name=\"profile_requiring_plugin\">يتطلب ملف التعريف %s تثبيت المكون الإضافي %s، لكن لم يتم العثور عليه.</string>\n    <string name=\"missing_plugin\">في عداد المفقودين المكونات في</string>\n    <string name=\"confirm\">أكد</string>\n    <string name=\"subscription_user_agent\">وكيل المستخدم</string>\n    <string name=\"update_when_connected_only_sum\">منع تسرب عنوان IP</string>\n    <string name=\"update_when_connected_only\">التحديث فقط عند الاتصال</string>\n    <string name=\"auto_update_delay\">تأخير التحديث التلقائي (بالدقائق)</string>\n    <string name=\"auto_update\">التحديث التلقائي</string>\n    <string name=\"update_settings\">تحديث الاعدادات</string>\n    <string name=\"raw\">الخام</string>\n    <string name=\"deduplication_sum\">قم بإزالة التكوينات المكررة عند التحديث</string>\n    <string name=\"force_vmess_aead_sum\">أعد كتابة تكوين VMess بطريقة المصادقة غير الآمنة إلى AEAD (alterId 0)</string>\n    <string name=\"force_vmess_aead\">فرض VMess AEAD</string>\n    <string name=\"force_resolve_sum\">حل جميع أسماء المجالات إلى عناوين IP عند التحديث. سيتم إلحاق المضيف و SNI تلقائيًا إن أمكن</string>\n    <string name=\"force_resolve\">الحل القسري</string>\n    <string name=\"delete_group_prompt\">هل أنت متأكد أنك تريد إزالة هذه المجموعة؟</string>\n    <string name=\"subscription_type\">نوع الاشتراك</string>\n    <string name=\"group_type\">نوع المجموعة</string>\n    <string name=\"subscription_settings\">إعدادات الاشتراك</string>\n    <string name=\"subscription\">الاشتراك</string>\n    <string name=\"group_settings\">إعدادات المجموعة</string>\n    <string name=\"group_basic\">أساسي</string>\n    <string name=\"ss_aead_provider\">مزود Shadowsocks AEAD</string>\n    <string name=\"trojan_provider\">Trojan مزود</string>\n    <string name=\"protocol_settings\">إعدادات البروتوكول</string>\n    <string name=\"bypass_lan_in_core_only_sum\">إذا لم يعمل Lineage\\'s السماح لعملاء نقطة الاتصال باستخدام شبكات VPN، فجرّب ذلك.</string>\n    <string name=\"bypass_lan_in_core_only\">تجاوز LAN في النواة فقط</string>\n    <string name=\"append_http_proxy_sum\">سيتم استخدام بروکسی HTTP مباشرة من (المتصفح/ بعض التطبيقات المدعومة)، دون المرور عبر جهاز NIC الافتراضي (Android 10+)</string>\n    <string name=\"append_http_proxy\">إلحاق بروکسی HTTP ب VPN</string>\n    <string name=\"connection_test_timeout\">نفذ الوقت</string>\n    <string name=\"connection_test_unreachable\">لا يمكن الوصول إليه</string>\n    <string name=\"connection_test_refused\">الاتصال مرفوض</string>\n    <string name=\"connection_test_domain_not_found\">المجال لم يتم العثور</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing غير متوفر</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing غير متوفر</string>\n    <string name=\"connection_test_clear_results\">المقاصة نتائج الاختبار</string>\n    <string name=\"connection_test\">اختبار الاتصال</string>\n    <string name=\"clear_traffic_statistics\">مسح إحصائيات حركة المرور</string>\n    <string name=\"always_show_address_sum\">اعرض دائمًا عنوان الخادم على بطاقة التكوين</string>\n    <string name=\"always_show_address\">اعرض العنوان دائمًا</string>\n    <string name=\"unavailable\">غير متوفره</string>\n    <string name=\"standard\">معيار</string>\n    <string name=\"probe_interval\">الفاصل الزمني لرصد الموازن</string>\n    <string name=\"api_port\">منفذ API</string>\n    <string name=\"random\">عشوائي</string>\n    <string name=\"list\">قائمة</string>\n    <string name=\"enable_log_sum\">من أجل إصلاح الخلل</string>\n    <string name=\"enable_log\">تمكين التقارير</string>\n    <string name=\"auto\">تلقائي</string>\n    <string name=\"disable\">إلغاء</string>\n    <string name=\"enable\">مكن</string>\n    <string name=\"follow_system\">اتبع النظام</string>\n    <string name=\"night_mode\">وضع الليل</string>\n    <string name=\"lines\">%d سطور</string>\n    <string name=\"route_warn\">تأكد من قراءة الوثائق قبل إضافة القواعد المخصصة، وإلا فقد لا تتمكن من الاتصال بالإنترنت.</string>\n    <string name=\"license\">الرخصة</string>\n    <string name=\"restart\">أعد تحميل خدمة البروکسی لتطبيق التغييرات</string>\n    <string name=\"apply\">تطبيق</string>\n    <string name=\"no\">لا</string>\n    <string name=\"yes\">نعم</string>\n    <string name=\"unsaved_changes_prompt\">لم يتم حفظ التغييرات. هل تريد حفظ؟</string>\n    <string name=\"ss_cat\">إعدادات Shadowsocks</string>\n    <string name=\"proxy_cat\">اعدادات الخادم</string>\n    <string name=\"plugin_auto_connect_unlock_only\">قد لا يعمل هذا المكون الإضافي مع الاتصال التلقائي</string>\n    <string name=\"plugin_untrusted\">تحذير: يبدو أن هذا المكون الإضافي لا يأتي من مصدر موثوق ومعروف.</string>\n    <string name=\"plugin_unknown\">مكون إضافي غير معروف %s</string>\n    <string name=\"plugin_disabled\">غير مفعّل</string>\n    <string name=\"plugin_configure\">تهيئة…</string>\n    <string name=\"plugin\">الإضافات</string>\n    <string name=\"ignore_battery_optimizations_sum\">قم بإزالة بعض القيود</string>\n    <string name=\"ignore_battery_optimizations\">تجاهل تحسينات البطارية</string>\n    <string name=\"donate_info\">انا احب المال</string>\n    <string name=\"donate\">تبرع</string>\n    <string name=\"deduplication\">احذف البيانات المكررة</string>\n    <string name=\"no_proxies_found_in_clipboard\">لم يتم العثور على بروکسیز في الحافظة</string>\n    <string name=\"no_proxies_found_in_file\">لم يتم العثور على بروکسیز في الملف</string>\n    <string name=\"no_proxies_found_in_subscription\">لم يتم العثور على بروکسیز في الاشتراك</string>\n    <string name=\"no_proxies_found\">لم يتم العثور على بروکسیز في الارتباط</string>\n    <string name=\"error_title\">خطأ</string>\n    <string name=\"cleartext_http_warning\">حركة مرور نص واضح HTTP غير آمنة</string>\n    <string name=\"subscriptions\">الاشتراكات</string>\n    <string name=\"not_connected\">غير متصل</string>\n    <string name=\"vpn_connected\">متصل، انقر للتحقق من الاتصال</string>\n    <string name=\"connecting\">يتصل…</string>\n    <string name=\"deprecated\">مهجور</string>\n    <string name=\"insecure\">غير آمن</string>\n    <string name=\"insecure_warn_sum\">مقدم من مجتمع مطوري Qv2ray</string>\n    <string name=\"insecure_warn\">استشارات أمنية</string>\n    <string name=\"undo\">تراجع</string>\n    <plurals name=\"added\">\n        <item quantity=\"zero\">مضاف</item>\n        <item quantity=\"one\">%d تمت إضافة البروکسیز</item>\n        <item quantity=\"two\"/>\n        <item quantity=\"few\"/>\n        <item quantity=\"many\"/>\n        <item quantity=\"other\"/>\n    </plurals>\n    <plurals name=\"removed\">\n        <item quantity=\"zero\">محذوف</item>\n        <item quantity=\"one\">%d العناصر التي تمت إزالتها</item>\n        <item quantity=\"two\"/>\n        <item quantity=\"few\"/>\n        <item quantity=\"many\"/>\n        <item quantity=\"other\"/>\n    </plurals>\n    <string name=\"add_profile_methods_manual_settings\">الإعدادات اليدوية</string>\n    <string name=\"experimental_no_termination_signal\">لا ترسل إشارة إنهاء اتصال فردية مزدوجة لاتصال TCP عند نقلها عبر VMess. سيؤدي هذا إلى كسر بعض التطبيقات.</string>\n    <string name=\"experimental_authenticated_length\">لم يعد طول كل شحنة مرنًا. يتطلب هذا الاختبار خادمًا ويستخدم العميل نفس إصدار v2ray-core. من المتوقع المزيد من التحديثات الفورية في هذا الاختبار.</string>\n    <string name=\"experimental_settings\">تجريبي</string>\n    <string name=\"route_need_fds\">تعتمد قاعدة التوجيه %s على تفعيل خدمة كاشف المقدمة، ولكن لم يتم تمكينها.</string>\n    <string name=\"route_need_vpn\">تعتمد قاعدة التوجيه %s على أن تكون VPN سارية المفعول، لذلك يتم تجاهلها.</string>\n    <string name=\"foreground_status\">حالة المقدمة</string>\n    <string name=\"foreground_detector_summary\">تدعم هذه الخدمة قواعد التوجيه بناءً على حالة المقدمة التطبيقات. إذا لم يتم بدء تشغيل، تكون النتيجة دائما في خلفية.</string>\n    <string name=\"background\">خلفية</string>\n    <string name=\"foreground\">المقدمة</string>\n    <string name=\"foreground_detector\">كاشف المقدمة</string>\n    <string name=\"route_for\">حكم ل %s</string>\n    <string name=\"app_no_launcher\">التطبيق ليس له واجهة.</string>\n    <string name=\"copy_failed\">فشل النسخ.</string>\n    <string name=\"copy_success\">تم النسخ بنجاح!</string>\n    <string name=\"create_rule\">إنشاء قاعدة</string>\n    <string name=\"open_market\">السوق المفتوح</string>\n    <string name=\"open_settings\">أفتح الإعدادات</string>\n    <string name=\"open_app\">افتح التطبيق</string>\n    <string name=\"copy_package_name\">نسخ اسم الحزمة</string>\n    <string name=\"copy_label\">نسخ الاسم</string>\n    <string name=\"traffic_holder\">افتح VPN لتسجيل إحصائيات حركة المرور</string>\n    <string name=\"udp_connections\">%d اتصالات UDP</string>\n    <string name=\"tcp_connections\">%d اتصالات TCP</string>\n    <string name=\"traffic_stats\">إحصائيات</string>\n    <string name=\"traffic_active\">نشط</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (مكون أندرويد الإضافي Shadowsocks)</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Simple Obfs (مكون أندرويد الإضافي Shadowsocks)</string>\n    <string name=\"plugin_exists_but_on_shit_system\">يتطلب الملف التعریف %s المكوّن الإضافي %s، لكن بائع المعدات المملوكة لك (عادةً عمالقة المراقبة وصانع البرامج الضارة) تلاعب بجهاز أندرويد، مما يجعل المكون الإضافي غير قابل للاستخدام.</string>\n    <string name=\"group_order_by_delay\">عن طريق التأخير</string>\n    <string name=\"group_order_by_name\">بالاسم</string>\n    <string name=\"group_order_origin\">أصل</string>\n    <string name=\"group_order\">ترتيب</string>\n    <string name=\"hysteria_disable_mtu_discovery\">تعطيل اكتشاف MTU للمسار</string>\n    <string name=\"hysteria_connection_receive_window\">نافذة تلقي اتصال QUIC</string>\n    <string name=\"hysteria_stream_receive_window\">نافذة تلقي دفق QUIC</string>\n    <string name=\"ss_stream_provider\">مزود تيار شادوسوكس</string>\n    <string name=\"menu_traffic\">مرور</string>\n    <string name=\"no_statistics\">لا توجد إحصاءات حتى الآن</string>\n    <string name=\"traffic_statistics_summary\">قد يزيد من استهلاك الطاقة</string>\n    <string name=\"clear_logcat\">تقارير أداء واضحة</string>\n    <string name=\"menu_log\">السجلات</string>\n    <string name=\"menu_tools\">أدوات</string>\n    <string name=\"translate_platform\">منصة الترجمة</string>\n    <string name=\"ssh_private_key_passphrase\">عبارة مرور المفتاح الخاص</string>\n    <string name=\"ssh_private_key\">مفتاح خاص</string>\n    <string name=\"ssh_public_key\">المفتاح العمومي</string>\n    <string name=\"ssh_auth_type_none\">لا شيء</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-be/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"logcat\">Экспартаваць інфармацыю пра адладку</string>\n    <string name=\"version_x\">Версія (%s)</string>\n    <string name=\"app_version\">Версія</string>\n    <string name=\"oss_licenses\">Ліцэнзіі з адкрытым зыходным кодам</string>\n    <string name=\"telegram\">Канал абнаўлення тэлеграм</string>\n    <string name=\"github\">Зыходны код</string>\n    <string name=\"project\">Праект</string>\n    <string name=\"app_desc\">Універсальны набор інструментаў проксі для Android, напісаны на Kotlin.</string>\n    <string name=\"ws_path\">Шлях WebSocket</string>\n    <string name=\"ws_host\">Хост WebSocket</string>\n    <string name=\"header_type\">Тып загалоўка</string>\n    <string name=\"network\">Сетка</string>\n    <string name=\"security\">Шыфраванне транспартнага ўзроўню</string>\n    <string name=\"alter_id\">Змяніць ID</string>\n    <string name=\"uuid\">Ідентифікатор карыстальніка</string>\n    <string name=\"obfs_param\">Obfs Парам</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"protocol_param\">Пратакол Param</string>\n    <string name=\"protocol\">Пратакол</string>\n    <string name=\"enc_method\">Метад шыфравання</string>\n    <string name=\"password\">Пароль</string>\n    <string name=\"key_opt\">Ключ (неабавязкова)</string>\n    <string name=\"password_opt\">Пароль (неабавязкова)</string>\n    <string name=\"username_opt\">Імя карыстальніка (неабавязкова)</string>\n    <string name=\"username\">Імя карыстальніка</string>\n    <string name=\"server_port\">Аддалены порт</string>\n    <string name=\"server_address\">Сервер</string>\n    <string name=\"profile_name\">Імя профілю</string>\n    <string name=\"connection_test_url\">Тэст URL падлучэння</string>\n    <string name=\"transproxy_mode\">Рэжым Transproxy</string>\n    <string name=\"require_transproxy\">Уключыць Transproxy Уваходныя</string>\n    <string name=\"inbound_settings\">Уключыць Transproxy Уваходныя</string>\n    <string name=\"port_local_dns\">Лакальны порт DNS</string>\n    <string name=\"dns_hosts\">Перапісаць дамен</string>\n    <string name=\"fakedns_message\">Можа выклікаць перазапуск іншых прыкладанняў для паўторнага падлучэння да сеткі пасля спынення проксі</string>\n    <string name=\"enable_fakedns\">Уключыць FakeDNS</string>\n    <string name=\"dns_routing_message\">Выкарыстоўвайце прамы DNS для маршрутаў па -за проксі. У гэтым выпадку верагодна ўцечка DNS</string>\n    <string name=\"enable_dns_routing\">Уключыць маршрутызацыю DNS</string>\n    <string name=\"direct_dns\">Прамы DNS</string>\n    <string name=\"remote_dns\">Аддалены DNS</string>\n    <string name=\"cag_dns\">Налады DNS</string>\n    <string name=\"connection_test_error_status_code\">Код памылкі: #%d</string>\n    <string name=\"connection_test_fail\">Інтэрнэт недаступны</string>\n    <string name=\"connection_test_error\">Не атрымалася: %s</string>\n    <string name=\"connection_test_available_http\">Поспех: поціск рукі HTTP заняў %dms</string>\n    <string name=\"connection_test_available\">Поспех: поціск рукі HTTPS заняў %dms</string>\n    <string name=\"connection_test_testing\">Тэставанне…</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"speed_detail\">Проксі : %1$s↑ %2$s↓\n\\nПрамая : %3$s↑ %4$s↓</string>\n    <string name=\"allow_insecure_sum\">Адключыць праверку сертыфікатаў. Калі ўключана, гэтая канфігурацыя такая ж бяспечная, як і адкрыты тэкст</string>\n    <string name=\"allow_insecure\">Дазволіць небяспечна</string>\n    <string name=\"security_settings\">Налады бяспекі</string>\n    <string name=\"show_direct_speed_sum\">У апавяшчэнні таксама паказвайце хуткасць руху без проксі</string>\n    <string name=\"show_direct_speed\">Паказаць прамую хуткасць</string>\n    <string name=\"show_stop_sum\">Калі вы не хочаце выкарыстоўваць кнопку панэлі апавяшчэнняў у якасці пераключальніка злучэння</string>\n    <string name=\"show_stop\">Паказаць кнопку прыпынку</string>\n    <string name=\"cag_misc\">Розныя налады</string>\n    <string name=\"speed_interval\">Інтэрвал абнаўлення апавяшчэнняў аб хуткасці</string>\n    <string name=\"ws_browser_forwarding_sum\">Перадайце адпаведныя WebSockets праз браўзэр.</string>\n    <string name=\"ws_browser_forwarding\">Пераадрасацыя браўзэра</string>\n    <string name=\"cag_ws\">Налады WebSocket</string>\n    <string name=\"require_http\">Уключыць увод HTTP</string>\n    <string name=\"general_settings\">Налады прыкладання</string>\n    <string name=\"allow_access_sum\">Прывязаць уваходныя серверы да 0.0.0.0</string>\n    <string name=\"allow_access\">Дазволіць злучэнне з лакальнай сеткі</string>\n    <string name=\"cag_route\">Налады маршруту</string>\n    <string name=\"port_transproxy\">Transproxy порт</string>\n    <string name=\"port_http\">Проксі -порт HTTP</string>\n    <string name=\"port_proxy\">Порт проксі -сервера SOCKS5</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode_proxy\">Толькі проксі</string>\n    <string name=\"service_mode\">Рэжым абслугоўвання</string>\n    <string name=\"group_duplicate\">Дублікат:\n\\n%s</string>\n    <string name=\"group_deleted\">Выдалена:\n\\n%s</string>\n    <string name=\"group_changed\">Абноўлена:\n\\n%s</string>\n    <string name=\"group_added\">Дададзена:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">Вы ўпэўненыя, што хочаце выдаліць гэтую групу\\?</string>\n    <string name=\"group_diff\">Розніца (%s)</string>\n    <string name=\"group_show_diff\">Розніца</string>\n    <string name=\"group_updated\">%s: Абноўлены %d проксі</string>\n    <string name=\"group_no_difference\">%s: няма розніцы</string>\n    <string name=\"group_status_proxies_subscription\">%d проксі | Абноўлена %s</string>\n    <string name=\"group_status_proxies\">%d проксі</string>\n    <string name=\"group_status_empty_subscription\">Пакуль не абноўлена</string>\n    <string name=\"group_status_empty\">Пустая група</string>\n    <string name=\"group_edit_subscription\">Рэдагаваць падпіску</string>\n    <string name=\"group_edit\">Рэдагаваць групу</string>\n    <string name=\"group_create_subscription\">Стварыць падпіску</string>\n    <string name=\"group_create\">Стварыць групу</string>\n    <string name=\"group_name_required\">Патрабуецца назва групы</string>\n    <string name=\"group_subscription_link\">Спасылка на падпіску</string>\n    <string name=\"group_update\">Абнаўленне</string>\n    <string name=\"group_name\">Назва групы</string>\n    <string name=\"menu_traffic\">Трафік</string>\n    <string name=\"tile_title\">Перамыкач</string>\n    <string name=\"quick_toggle\">Пераключыць</string>\n    <string name=\"group_default\">Разгрупаваны</string>\n    <string name=\"document\">Дакумент</string>\n    <string name=\"theme\">Тэма</string>\n    <string name=\"menu_about\">Пра</string>\n    <string name=\"menu_group\">Група</string>\n    <string name=\"menu_configuration\">Канфігурацыя</string>\n    <string name=\"logcat_summary\">Такі карысны вельмі нічога сабе</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"group_subscription_link\">Abonnement-Link</string>\n    <string name=\"group_update\">Aktualisieren</string>\n    <string name=\"group_name\">Gruppen name</string>\n    <string name=\"tile_title\">Umschalter</string>\n    <string name=\"quick_toggle\">Umschalten</string>\n    <string name=\"group_default\">Ungruppiert</string>\n    <string name=\"document\">Dokument</string>\n    <string name=\"theme\">Style</string>\n    <string name=\"menu_about\">über</string>\n    <string name=\"menu_group\">Gruppe</string>\n    <string name=\"menu_configuration\">Konfiguration</string>\n    <string name=\"app_version\">Version</string>\n    <string name=\"logcat_summary\">So nützlich sehr wow</string>\n    <string name=\"logcat\">Debug-Informationen exportieren</string>\n    <string name=\"version_x\">Version (%s)</string>\n    <string name=\"oss_licenses\">Quelloffene Lizenzen</string>\n    <string name=\"telegram\">Telegramm-Update-Kanal</string>\n    <string name=\"github\">Quellcode</string>\n    <string name=\"project\">Projekt</string>\n    <string name=\"app_desc\">Die universelle Proxy-Toolchain für Android, geschrieben in Kotlin.</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"github\">Código fuente</string>\n    <string name=\"project\">Proyecto</string>\n    <string name=\"app_desc\">La cadena de herramientas de proxy universal para Android, escrita en Kotlin.</string>\n    <string name=\"version_x\">Versión (%s)</string>\n    <string name=\"app_version\">Versión</string>\n    <string name=\"always_show_address_sum\">Mostrar siempre la dirección del servidor en la tarjeta de configuración</string>\n    <string name=\"enable_log\">Habilitar registro</string>\n    <string name=\"disable\">Desactivar</string>\n    <string name=\"enable\">Permitir</string>\n    <string name=\"night_mode\">Modo nocturno</string>\n    <string name=\"route_warn\">Asegúrese de haber leído la documentación antes de agregar reglas personalizadas; de lo contrario, es posible que no pueda conectarse a Internet.</string>\n    <string name=\"restart\">Vuelva a cargar el servicio de proxy para aplicar cambios</string>\n    <string name=\"no\">No</string>\n    <string name=\"yes\">sí</string>\n    <string name=\"ss_cat\">Configuración de Shadowsocks</string>\n    <string name=\"proxy_cat\">Configuración del servidor</string>\n    <string name=\"plugin_auto_connect_unlock_only\">Es posible que este complemento no funcione con Auto Connect</string>\n    <string name=\"plugin_untrusted\">Advertencia: este complemento no parece provenir de una fuente confiable conocida.</string>\n    <string name=\"plugin_disabled\">Desactivado</string>\n    <string name=\"donate_info\">Amo el dinero</string>\n    <string name=\"donate\">Donar</string>\n    <string name=\"no_proxies_found_in_clipboard\">No se encontraron proxies en el portapapeles</string>\n    <string name=\"no_proxies_found_in_file\">No se encontraron proxies en el archivo</string>\n    <string name=\"no_proxies_found_in_subscription\">No se encontraron proxies en la suscripción</string>\n    <string name=\"no_proxies_found\">No se encontraron proxies en el enlace</string>\n    <string name=\"error_title\">Error</string>\n    <string name=\"cleartext_http_warning\">El tráfico HTTP no cifrado es inseguro</string>\n    <string name=\"subscriptions\">Suscripciones</string>\n    <string name=\"not_connected\">No conectado</string>\n    <string name=\"ignore_battery_optimizations_sum\">Eliminar algunas restricciones</string>\n    <string name=\"ignore_battery_optimizations\">Ignorar las optimizaciones de la batería</string>\n    <string name=\"action_download\">DESCARGAR</string>\n    <string name=\"action_import_msg\">Importado con éxito!</string>\n    <string name=\"action_create_group\">Grupo vacio</string>\n    <string name=\"action_import_file\">Importar desde un archivo</string>\n    <string name=\"action_import\">Importar desde el portapapeles</string>\n    <string name=\"action_export_clipboard\">Exportar al portapapeles</string>\n    <string name=\"action_export_err\">Fallo la exportación.</string>\n    <string name=\"action_export\">Exportar</string>\n    <string name=\"action_export_msg\">Exportación exitosa!</string>\n    <string name=\"select_profile\">Seleccionar perfil</string>\n    <string name=\"add_profile\">Añadir perfil</string>\n    <string name=\"share\">Enviar</string>\n    <string name=\"edit\">Editar</string>\n    <string name=\"settings\">Ajustes</string>\n    <string name=\"scanning\">Escaneando…</string>\n    <string name=\"license\">Licencias</string>\n    <string name=\"password_opt\">Contraseña (opcional)</string>\n    <string name=\"username_opt\">Nombre de usuario (opcional)</string>\n    <string name=\"unsaved_changes_prompt\">Cambios no guardados. ¿Quieres guardar\\?</string>\n    <string name=\"connecting\">Conectando…</string>\n    <string name=\"connection_test_clear_results\">Limpiar resultados</string>\n    <string name=\"connection_test_refused\">Conexión denegada</string>\n    <string name=\"delete\">Eliminar</string>\n    <string name=\"custom_config\">Configuración personalizada</string>\n    <string name=\"route_add\">Crear ruta</string>\n    <string name=\"port_local_dns\">Puerto DNS local</string>\n    <string name=\"dns_hosts\">Reescritura de dominio</string>\n    <string name=\"fakedns_message\">Puede hacer que otras aplicaciones deban reiniciarse para volver a conectarse a la red después de que se detenga el proxy</string>\n    <string name=\"group_delete_confirm_prompt\">Estás seguro de eliminar este grupo\\?</string>\n    <string name=\"action_import_err\">Importacion fallida.</string>\n    <string name=\"add_profile_methods_scan_qr_code\">Importar con QR</string>\n    <string name=\"always_show_address\">Mostrar siempre la dirección</string>\n    <string name=\"auto_update\">Actualización automática</string>\n    <string name=\"auto_connect\">Auto conectar</string>\n    <string name=\"subscription_import_message\">¿Confirma que desea importar la suscripción %s\\? Si viene de una fuente que no es de confianza, hacer esto puede resultar en que se filtre su IP y este comportamiento.</string>\n    <string name=\"subscription_import\">Importar subscripción</string>\n    <string name=\"group_filter\">Filtrar</string>\n    <string name=\"group_filter_tags\">Etiquetas</string>\n    <string name=\"service_subscription\">Servicio actualizado</string>\n    <string name=\"clear_profiles_message\">¿Estás seguro de que deseas borrar este grupo\\?</string>\n    <string name=\"install_from_play_store\">Instalar de la Tienda</string>\n    <string name=\"confirm\">Confirmar</string>\n    <string name=\"install_from_fdroid\">Instalar de F-Droid</string>\n    <string name=\"group_filter_groups\">Grupos</string>\n    <string name=\"profile_import\">Importar perfil</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"group_update\">Actualizar</string>\n    <string name=\"group_create_subscription\">Crear subscripción</string>\n    <string name=\"menu_group\">Grupo</string>\n    <string name=\"logcat\">Exportar información depurada</string>\n    <string name=\"menu_about\">Acerca de</string>\n    <string name=\"group_name_required\">Requiere nombre de grupo</string>\n    <string name=\"menu_configuration\">Configuraciones</string>\n    <string name=\"document\">Sagernet.org</string>\n    <string name=\"group_subscription_link\">Enlace de subscripción</string>\n    <string name=\"group_name\">Nombre del grupo</string>\n    <string name=\"group_create\">Crear grupo</string>\n    <string name=\"group_edit_subscription\">Editar subscripción</string>\n    <string name=\"group_edit\">Editar grupo</string>\n    <string name=\"oss_licenses\">Licencias de código abierto</string>\n    <string name=\"telegram\">Canal de actualizaciones</string>\n    <string name=\"ooc_missing_protocol\">La suscripción requiere soporte para el protocolo %s, pero no se puede encontrar. Se ignorarán los perfiles no admitidos.</string>\n    <string name=\"raw\">Crudo</string>\n    <string name=\"deduplication_sum\">Eliminar configuraciones duplicadas al actualizar</string>\n    <string name=\"force_vmess_aead_sum\">Reescriba la configuración de VMess con un método de autenticación inseguro en AEAD (alterId 0)</string>\n    <string name=\"clipboard_empty\">El portapapeles está vacío</string>\n    <string name=\"connect\">Conectar</string>\n    <string name=\"profile_empty\">Por favor seleccione un perfil</string>\n    <string name=\"reboot_required\">No se pudo iniciar el servicio VPN. Es posible que deba reiniciar su dispositivo.</string>\n    <string name=\"vpn_permission_denied\">Permiso denegado para crear un servicio VPN</string>\n    <string name=\"cag_ws\">Configuración de WebSocket</string>\n    <string name=\"require_http\">Habilitar HTTP entrante</string>\n    <string name=\"general_settings\">Ajustes de la Aplicacion</string>\n    <string name=\"hysteria_obfs\">Contraseña de Obfuscation</string>\n    <string name=\"route_reverse_redirect\">Redireccionamiento inverso</string>\n    <string name=\"route_reverse\">Proxy inverso</string>\n    <string name=\"route_bypass_ip\">Regla de IP para %s</string>\n    <string name=\"route_block\">Block</string>\n    <string name=\"hysteria_download_mbps\">Velocidad máxima de descarga (en Mbps)</string>\n    <string name=\"hysteria_upload_mbps\">Velocidad de subida máxima (en Mbps)</string>\n    <string name=\"hysteria_auth_payload\">Autenticación del Payload</string>\n    <string name=\"hysteria_auth_type\">Tipo de autenticación</string>\n    <string name=\"apps_message\">%d Aplicaciones</string>\n    <string name=\"select_apps\">Seleccionar aplicaciones</string>\n    <string name=\"subscription_used\">%s utilizado</string>\n    <string name=\"group_filter_tags_nf\">No hay etiquetas en esta suscripción</string>\n    <string name=\"group_filter_groups_nf\">No hay grupos en esta suscripción</string>\n    <string name=\"group_filter_ns\">Se requiere suscripción a OOCv1 para usar la función de filtrado</string>\n    <string name=\"action_learn_more\">APRENDE MÁS</string>\n    <string name=\"profile_import_message\">Confirma que desea importar el perfil %s\\?</string>\n    <string name=\"route_bypass_domain\">Regla de dominio para %s</string>\n    <string name=\"route_reset\">Reiniciar</string>\n    <string name=\"empty_route_notice\">Establece algunas reglas antes de guardar</string>\n    <string name=\"empty_route\">Ruta vacía</string>\n    <string name=\"route_profile\">Seleccionar un perfil…</string>\n    <string name=\"route_bypass\">Omitir</string>\n    <string name=\"route_proxy\">Proxy</string>\n    <string name=\"route_name\">Nombre de ruta</string>\n    <string name=\"delete_route_prompt\">¿Estás seguro de que deseas eliminar esta ruta\\?</string>\n    <string name=\"early_data_header_name\">Nombre del encabezado de datos iniciales</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Cadena de certificado fijada</string>\n    <string name=\"certificates\">Certificados</string>\n    <string name=\"sni\">SNI (Indicación del nombre del servidor)</string>\n    <string name=\"tls\">Usar TLS</string>\n    <string name=\"grpc_service_name\">gRPC Nombre de servicio</string>\n    <string name=\"kcp_seed\">Semilla mKCP</string>\n    <string name=\"quic_key\">Llave QUIC</string>\n    <string name=\"alter_id\">Alterar ID</string>\n    <string name=\"uuid\">ID de usuario</string>\n    <string name=\"obfs_param\">Parámetros de obfs</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"protocol_param\">Parámetros de protocolo</string>\n    <string name=\"protocol\">Protocolo</string>\n    <string name=\"dns_routing_message\">Resuelva dominios en rutas de bypass con Direct DNS. Tenga en cuenta las posibles fugas de DNS</string>\n    <string name=\"enable_dns_routing\">Habilitar el enrutamiento DNS</string>\n    <string name=\"direct_dns\">DNS directo</string>\n    <string name=\"remote_dns\">DNS remoto</string>\n    <string name=\"cag_dns\">Configuración de DNS</string>\n    <string name=\"connection_test_error_status_code\">Código de error: #%d</string>\n    <string name=\"connection_test_fail\">Internet no disponible</string>\n    <string name=\"connection_test_error\">Error al detectar la conexión a Internet: %s</string>\n    <string name=\"connection_test_available_http\">Éxito: el protocolo de enlace HTTP tomó %dms</string>\n    <string name=\"connection_test_available\">Éxito: el protocolo de enlace HTTPS tomó %dms</string>\n    <string name=\"connection_test_testing\">Comprobando…</string>\n    <string name=\"allow_insecure_sum\">Desactive la verificación de certificados. Cuando está habilitada, esta configuración es tan segura como el texto sin formato</string>\n    <string name=\"allow_insecure\">Permitir inseguro</string>\n    <string name=\"allow_access\">Permitir conexiones desde la LAN</string>\n    <string name=\"group_status_empty_subscription\">Aún no está actualizado</string>\n    <string name=\"group_status_empty\">Grupo vacio</string>\n    <string name=\"logcat_summary\">Tan útil xd</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"clear_selections\">Borrar selecciones</string>\n    <string name=\"invert_selections\">Invertir selecciones</string>\n    <string name=\"search_apps\">Buscar…</string>\n    <string name=\"off\">Apagado</string>\n    <string name=\"on\">En</string>\n    <string name=\"proxied_apps_summary\">Configurar el modo VPN para aplicaciones seleccionadas</string>\n    <string name=\"proxied_apps\">Modo VPN de aplicaciones</string>\n    <string name=\"tcp_keep_alive_interval\">TCP mantiene el intervalo de entrega de paquetes activo</string>\n    <string name=\"mux_for_all_sum\">Si el servidor no es compatible con Mux, no podremos conectarnos a él.</string>\n    <string name=\"mux_for_all\">Habilite Mux para todos los posibles protocolos admitidos</string>\n    <string name=\"mux_concurrency\">Conexiones concurrentes de Mux</string>\n    <string name=\"mux_sum\">Mux está diseñado para reducir la latencia del protocolo de enlace de TCP, no para aumentar el rendimiento de la conexión. Usar Mux para ver videos, descargar o realizar pruebas de velocidad suele ser contraproducente</string>\n    <string name=\"enable_mux\">Habilitar multiplexor</string>\n    <string name=\"traffic_sniffing\">Habilitar el rastreo de tráfico</string>\n    <string name=\"domain_strategy\">Estrategia de resolución de dominio</string>\n    <string name=\"route_opt_block_ads\">Bloquear anuncios</string>\n    <string name=\"route_not_asset\">No es un archivo de activos: excepto .dat, pero %s</string>\n    <string name=\"route_asset_updated\">Actualizado</string>\n    <string name=\"route_asset_no_update\">Ninguna actualización</string>\n    <string name=\"route_asset_status\">Versión local: %s</string>\n    <string name=\"route_rules_official\">Oficial</string>\n    <string name=\"route_rules_provider\">Proveedor de activos de reglas</string>\n    <string name=\"route_assets\">Activos</string>\n    <string name=\"route_manage_assets\">Administrar activos de ruta</string>\n    <string name=\"menu_route\">Ruta</string>\n    <string name=\"metered_summary\">Sistema de sugerencias para tratar la VPN como medida</string>\n    <string name=\"metered\">Sugerencia medida</string>\n    <string name=\"only\">Solamente</string>\n    <string name=\"prefer\">Preferir</string>\n    <string name=\"ipv6\">Ruta IPv6</string>\n    <string name=\"edit_config\">Editar configuración</string>\n    <string name=\"config_type\">Tipo de configuración</string>\n    <string name=\"extra_headers\">Encabezados adicionales</string>\n    <string name=\"encryption\">Cifrado</string>\n    <string name=\"alpn\">Negociación del protocolo de capa de aplicación</string>\n    <string name=\"quic_security\">Seguridad QUIC</string>\n    <string name=\"http_path\">Ruta HTTP</string>\n    <string name=\"http_host\">Host HTTP</string>\n    <string name=\"ws_path\">Ruta de WebSocket</string>\n    <string name=\"ws_host\">Host de WebSocket</string>\n    <string name=\"header_type\">Tipo de encabezado</string>\n    <string name=\"network\">La red</string>\n    <string name=\"security\">Cifrado de la capa de transporte</string>\n    <string name=\"enc_method\">Método de cifrado</string>\n    <string name=\"password\">Contraseña</string>\n    <string name=\"key_opt\">Clave (opcional)</string>\n    <string name=\"username\">Nombre de usuario</string>\n    <string name=\"server_port\">Puerto remoto</string>\n    <string name=\"server_address\">Servidor</string>\n    <string name=\"profile_name\">Nombre de perfil</string>\n    <string name=\"connection_test_url\">URL de prueba de conexión</string>\n    <string name=\"transproxy_mode\">Modo transproxy</string>\n    <string name=\"require_transproxy\">Habilitar Transproxy Inbound</string>\n    <string name=\"enable_fakedns\">Habilitar DNS falso</string>\n    <string name=\"speed_detail\">Proxy : %1$s↑ %2$s↓\n\\nDirecto : %3$s↑ %4$s↓</string>\n    <string name=\"security_settings\">Configuraciones de seguridad</string>\n    <string name=\"show_direct_speed_sum\">Muestre la velocidad del tráfico sin proxy en la notificación también</string>\n    <string name=\"show_direct_speed\">Mostrar velocidad directa</string>\n    <string name=\"show_stop_sum\">Si no desea utilizar Quick Tile como interruptor</string>\n    <string name=\"show_stop\">Mostrar botón de detener</string>\n    <string name=\"cag_misc\">Configuración miscelánea</string>\n    <string name=\"speed_interval\">Intervalo de actualización de notificación de velocidad</string>\n    <string name=\"ws_browser_forwarding_sum\">Reenvíe los WebSockets correspondientes a través del navegador.</string>\n    <string name=\"ws_browser_forwarding\">Reenvío del navegador</string>\n    <string name=\"inbound_settings\">Configuración entrante</string>\n    <string name=\"allow_access_sum\">Vincular servidores entrantes a 0.0.0.0</string>\n    <string name=\"cag_route\">Configuración de ruta</string>\n    <string name=\"port_transproxy\">Puerto transproxy</string>\n    <string name=\"port_http\">Puerto proxy HTTP</string>\n    <string name=\"port_proxy\">Puerto proxy SOCKS5</string>\n    <string name=\"service_mode_proxy\">Proxy solamente</string>\n    <string name=\"group_show_diff\">Diferencia</string>\n    <string name=\"group_updated\">%s: Actualizar %d Proxys</string>\n    <string name=\"group_no_difference\">%s: sin diferencia</string>\n    <string name=\"group_status_proxies\">%d Proxys</string>\n    <string name=\"menu_traffic\">Tráfico</string>\n    <string name=\"tile_title\">Conmutador</string>\n    <string name=\"group_default\">Desagrupado</string>\n    <string name=\"plugin_exists_but_on_shit_system\">El perfil %s requiere el complemento %s, pero su proveedor de equipo patentado (generalmente los gigantes del capital de vigilancia y el fabricante de malware) manipuló su Android, haciendo que el complemento sea inutilizable.</string>\n    <string name=\"force_vmess_aead\">Fuerza VMess AEAD</string>\n    <string name=\"force_resolve\">Forzar resolución</string>\n    <string name=\"delete_group_prompt\">¿Estás seguro de que quieres eliminar este grupo\\?</string>\n    <string name=\"subscription_type\">Tipo de suscripción</string>\n    <string name=\"group_type\">Tipo de grupo</string>\n    <string name=\"subscription_settings\">Configuración de suscripción</string>\n    <string name=\"auto_update_delay\">Retraso de actualización automática (en minutos)</string>\n    <string name=\"update_when_connected_only\">Actualizar solo cuando esté conectado</string>\n    <string name=\"update_when_connected_only_sum\">Evite la fuga de direcciones IP</string>\n    <string name=\"subscription_user_agent\">Agente de usuario</string>\n    <string name=\"missing_plugin\">Falta Complemento</string>\n    <string name=\"profile_requiring_plugin\">El perfil %s requiere que se instale el complemento %s, pero no se encontró.</string>\n    <string name=\"download\">Descargar</string>\n    <string name=\"ooc_subscription_token_invalid\">Token OOCv1 no válido</string>\n    <string name=\"update_subscription_warning\">El proxy no está conectado, ¿está seguro de que desea continuar con la actualización\\?</string>\n    <string name=\"ooc_warning\">Advertencia</string>\n    <string name=\"subscription_update\">Actualización de suscripción</string>\n    <string name=\"subscription_update_message\">Actualizando %s…</string>\n    <string name=\"experimental_settings\">Experimental</string>\n    <string name=\"subscription_traffic\">%s usado / %s restante</string>\n    <string name=\"apps\">Aplicaciones</string>\n    <string name=\"hysteria_stream_receive_window\">Ventana de recepción QUIC Stream</string>\n    <string name=\"experimental_no_termination_signal\">No envíe una señal de terminación dúplex simple de conexión para la conexión TCP cuando se transfiera a través de VMess. Esto romperá alguna aplicación.</string>\n    <string name=\"hysteria_connection_receive_window\">Ventana de recepción de la conexión QUIC</string>\n    <string name=\"hysteria_disable_mtu_discovery\">Deshabilitar el descubrimiento de MTU de ruta</string>\n    <string name=\"group_order\">Pedido</string>\n    <string name=\"group_order_origin\">Origen</string>\n    <string name=\"group_order_by_name\">Por nombre</string>\n    <string name=\"group_order_by_delay\">Por retraso</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Obfs simples (complemento de Android Shadowsocks)</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (complemento de Android Shadowsocks)</string>\n    <string name=\"traffic_active\">Activo</string>\n    <string name=\"traffic_stats\">Estadísticas</string>\n    <string name=\"copy_label\">Copiar nombre</string>\n    <string name=\"copy_package_name\">Copiar el nombre del paquete</string>\n    <string name=\"open_app\">Abre la app</string>\n    <string name=\"open_settings\">Configuración abierta</string>\n    <string name=\"open_market\">Mercado abierto</string>\n    <string name=\"create_rule\">Crear regla</string>\n    <string name=\"copy_failed\">Error de copia.</string>\n    <string name=\"app_no_launcher\">La aplicación no tiene interfaz.</string>\n    <string name=\"route_for\">Regla para %s</string>\n    <string name=\"foreground_detector\">Detector de primer plano</string>\n    <string name=\"foreground_status\">Estado de primer plano</string>\n    <string name=\"foreground\">Primer plano</string>\n    <string name=\"background\">Fondo</string>\n    <string name=\"route_need_vpn\">La regla de enrutamiento %s depende de que la VPN esté en vigor, por lo que se ignora.</string>\n    <string name=\"route_need_fds\">La regla de enrutamiento %s depende de que el servicio de detector de primer plano esté en vigor, pero no está habilitado.</string>\n    <string name=\"traffic_statistics_summary\">Puede aumentar el consumo de energía</string>\n    <string name=\"no_statistics\">Aún no hay estadísticas</string>\n    <string name=\"ssh_auth_type_none\">Ninguno</string>\n    <string name=\"ssh_public_key\">Llave pública</string>\n    <string name=\"ssh_private_key\">Llave privada</string>\n    <string name=\"ssh_private_key_passphrase\">Frase de contraseña de clave privada</string>\n    <string name=\"translate_platform\">Traducir plataforma</string>\n    <string name=\"menu_tools\">Herramientas</string>\n    <string name=\"menu_log\">Registros</string>\n    <string name=\"clear_logcat\">Borrar Logcat</string>\n    <string name=\"wireguard_local_address\">Dirección local</string>\n    <string name=\"wireguard_public_key\">Clave pública del mismo nivel</string>\n    <string name=\"wireguard_psk\">Clave precompartida de pares</string>\n    <string name=\"foreground_detector_summary\">Este servicio proporciona soporte para reglas de enrutamiento basadas en el estado de primer plano de las aplicaciones. Si no se inicia, el resultado siempre está en segundo plano.</string>\n    <string name=\"copy_success\">¡Copie el éxito!</string>\n    <string name=\"traffic_holder\">Abra VPN para registrar estadísticas de tráfico</string>\n    <string name=\"udp_connections\">%d conexiones UDP</string>\n    <string name=\"tcp_connections\">%d conexiones TCP</string>\n    <string name=\"update_settings\">Ajustes de actualización</string>\n    <plurals name=\"added\">\n        <item quantity=\"one\">Adicional</item>\n        <item quantity=\"other\">%d proxys agregados</item>\n    </plurals>\n    <string name=\"quick_toggle\">Alternar</string>\n    <string name=\"group_diff\">Diferencia (%s)</string>\n    <string name=\"group_changed\">Actualizado:\n\\n%s</string>\n    <string name=\"group_added\">Adicional:\n\\n%s</string>\n    <string name=\"experimental_authenticated_length\">Haga que la longitud de cada segmento de carga útil ya no sea maleable. Este experimento requiere que el servidor y el cliente utilicen la misma versión de v2ray-core. Se esperan más actualizaciones de última hora sobre este experimento.</string>\n    <string name=\"force_resolve_sum\">Resuelva todos los nombres de dominio a direcciones IP al actualizar. El host y el SNI se agregarán automáticamente si es posible</string>\n    <string name=\"subscription\">Suscripción</string>\n    <string name=\"group_settings\">Configuración de grupo</string>\n    <string name=\"group_basic\">Básico</string>\n    <string name=\"ss_stream_provider\">Proveedor de corriente de Shadowsocks</string>\n    <string name=\"ss_aead_provider\">Proveedor de Shadowsocks AEAD</string>\n    <string name=\"trojan_provider\">Proveedor de Trojan</string>\n    <string name=\"protocol_settings\">Configuración de protocolo</string>\n    <string name=\"bypass_lan_in_core_only_sum\">Si la opción Permitir que los clientes de hotspot de Lineage usen VPN no funciona, intente esto.</string>\n    <string name=\"bypass_lan_in_core_only\">Omitir LAN solo en el núcleo</string>\n    <string name=\"append_http_proxy_sum\">El proxy HTTP se utilizará directamente desde (navegador / algunas aplicaciones compatibles), sin pasar por el dispositivo NIC virtual (Android 10+)</string>\n    <string name=\"append_http_proxy\">Agregar proxy HTTP a VPN</string>\n    <string name=\"connection_test_timeout\">Se acabó el tiempo</string>\n    <string name=\"connection_test_unreachable\">Inalcanzable</string>\n    <string name=\"connection_test_domain_not_found\">dominio no encontrado</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing no disponible</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing no disponible</string>\n    <string name=\"connection_test\">Prueba de conexión</string>\n    <string name=\"clear_traffic_statistics\">Limpiar estadísticas de tráfico</string>\n    <string name=\"unavailable\">Indisponible</string>\n    <string name=\"standard\">Estándar</string>\n    <string name=\"probe_interval\">Intervalo de observación del equilibrador</string>\n    <string name=\"api_port\">Puerto API</string>\n    <string name=\"random\">Aleatorio</string>\n    <string name=\"list\">Lista</string>\n    <string name=\"enable_log_sum\">Para fines de depuración</string>\n    <string name=\"auto\">Automático</string>\n    <string name=\"follow_system\">Seguir el sistema</string>\n    <string name=\"lines\">%d líneas</string>\n    <string name=\"apply\">Solicitar</string>\n    <string name=\"plugin\">Complemento</string>\n    <string name=\"plugin_unknown\">Complemento desconocido %s</string>\n    <string name=\"plugin_configure\">Configurar …</string>\n    <string name=\"deduplication\">Deduplicación</string>\n    <string name=\"vpn_connected\">Conectado, toque para comprobar la conexión</string>\n    <string name=\"deprecated\">Obsoleto</string>\n    <string name=\"insecure\">Inseguro</string>\n    <string name=\"insecure_warn_sum\">Proporcionado por la comunidad de desarrolladores de Qv2ray</string>\n    <string name=\"insecure_warn\">Asesoramiento de seguridad</string>\n    <string name=\"undo\">Deshacer</string>\n    <plurals name=\"removed\">\n        <item quantity=\"one\">Remoto</item>\n        <item quantity=\"other\">%d elementos eliminados</item>\n    </plurals>\n    <string name=\"add_profile_methods_manual_settings\">Ajustes manuales</string>\n    <string name=\"share_qr_nfc\">Código QR</string>\n    <string name=\"delete_confirm_prompt\">¿Estás seguro de que deseas eliminar este perfil\\?</string>\n    <string name=\"profile_config\">Configuración de perfil</string>\n    <string name=\"file_manager_missing\">Su dispositivo carece de un selector de archivos estándar de Android; instale uno, como Archivos de materiales.</string>\n    <string name=\"action_export_file\">Exportar a archivo</string>\n    <string name=\"action_scan_china_apps\">Escanear aplicaciones de China</string>\n    <string name=\"action_from_link\">De suscripción</string>\n    <string name=\"clear_profiles\">Limpiar</string>\n    <string name=\"circular_reference_sum\">La ruta no puede contenerse a sí misma.</string>\n    <string name=\"circular_reference\">Referencia circular</string>\n    <string name=\"config_settings\">Configuraciones de configuración</string>\n    <string name=\"chain_settings\">Configuración de cadena</string>\n    <string name=\"balancer_strategy\">Estrategia</string>\n    <string name=\"balancer_type\">Tipo</string>\n    <string name=\"balancer\">equilibrador</string>\n    <string name=\"balancer_settings\">Configuración del equilibrador</string>\n    <string name=\"proxy_chain\">Cadena de proxy</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"stopping\">Apagando…</string>\n    <string name=\"stop\">Detenido</string>\n    <string name=\"service_failed\">Fallido:</string>\n    <string name=\"invalid_server\">Nombre de servidor no válido</string>\n    <string name=\"forward_success\">Proxy iniciado.</string>\n    <string name=\"service_proxy\">Servicio de proxy</string>\n    <string name=\"service_vpn\">Servicio VPN</string>\n    <string name=\"direct_boot_aware_summary\">La información de su perfil seleccionada estará menos protegida</string>\n    <string name=\"direct_boot_aware\">Permitir alternar en la pantalla de bloqueo</string>\n    <string name=\"auto_connect_summary\">Habilite el proxy en el inicio / actualización de la aplicación si se estaba ejecutando antes</string>\n    <string name=\"show_system_apps\">Mostrar aplicaciones del sistema</string>\n    <string name=\"bypass_apps\">Derivación</string>\n    <string name=\"route_opt_bypass_lan\">Omitir LAN</string>\n    <string name=\"speed\">%s / s</string>\n    <string name=\"service_mode\">Modo de servicio</string>\n    <string name=\"group_duplicate\">Duplicar:\n\\n%s</string>\n    <string name=\"group_deleted\">Eliminado:\n\\n%s</string>\n    <string name=\"group_status_proxies_subscription\">%d Proxys | Actualizado en %s</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"show_stop\">نمایش دکمۀ توقف</string>\n    <string name=\"cag_misc\">تنظیمات متفرقه</string>\n    <string name=\"speed_interval\">فاصله بروزرسانی اعلان سرعت</string>\n    <string name=\"ws_browser_forwarding_sum\">هدایت وب‌سوکت مربوطه از طریق مرورگر.</string>\n    <string name=\"ws_browser_forwarding\">بازارسال مرورگر</string>\n    <string name=\"cag_ws\">تنظیمات وب‌سوکت</string>\n    <string name=\"require_http\">فعال کردن ورودی HTTP</string>\n    <string name=\"general_settings\">تنظیمات برنامه</string>\n    <string name=\"inbound_settings\">تنظیمات ورودی</string>\n    <string name=\"allow_access_sum\">اتصال سرورهای ورودی به 0.0.0.0</string>\n    <string name=\"allow_access\">اجازه اتصال از طریق LAN</string>\n    <string name=\"cag_route\">تنظیمات مسیر</string>\n    <string name=\"port_transproxy\">پورت Transproxy</string>\n    <string name=\"port_http\">پورت پروکسی HTTP</string>\n    <string name=\"port_proxy\">پورت پروکسی SOCKS5</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode_proxy\">فقط پروکسی</string>\n    <string name=\"service_mode\">حالت سرویس</string>\n    <string name=\"group_duplicate\">کپی:\n\\n%s</string>\n    <string name=\"group_deleted\">حذف شده:\n\\n%s</string>\n    <string name=\"group_changed\">به روز شده:\n\\n%s</string>\n    <string name=\"group_added\">اضافه:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">آیا مطمئن هستید که می خواهید این گروه را حذف کنید؟</string>\n    <string name=\"group_diff\">تفاوت (%s)</string>\n    <string name=\"group_show_diff\">تفاوت</string>\n    <string name=\"group_updated\">%s: %d پروکسی به روز شد</string>\n    <string name=\"group_no_difference\">%s: تفاوتی ندارد</string>\n    <string name=\"group_status_proxies_subscription\">%d پروکسی | به روز شده در %s</string>\n    <string name=\"group_status_proxies\">%d پروکسی ها</string>\n    <string name=\"group_status_empty_subscription\">هنوز به روز نشده است</string>\n    <string name=\"group_status_empty\">گروه خالی</string>\n    <string name=\"group_edit_subscription\">ویرایش اشتراک</string>\n    <string name=\"group_edit\">ویرایش گروه</string>\n    <string name=\"group_create_subscription\">اشتراک ایجاد کنید</string>\n    <string name=\"group_create\">ایجاد گروه</string>\n    <string name=\"group_name_required\">نام گروه لازم است</string>\n    <string name=\"group_subscription_link\">لینک اشتراک</string>\n    <string name=\"group_update\">بروزرسانی</string>\n    <string name=\"group_name\">نام گروه</string>\n    <string name=\"tile_title\">تعویض کننده</string>\n    <string name=\"quick_toggle\">تغییر وضعیت</string>\n    <string name=\"group_default\">گروه‌بندی نشده</string>\n    <string name=\"document\">مستندات</string>\n    <string name=\"theme\">قالب</string>\n    <string name=\"menu_about\">درباره</string>\n    <string name=\"menu_group\">گروه</string>\n    <string name=\"logcat\">گرفتن گزارش اشکال‌زدایی</string>\n    <string name=\"version_x\">نسخه (%s)</string>\n    <string name=\"app_version\">نسخه</string>\n    <string name=\"oss_licenses\">مجوزهای منبع باز</string>\n    <string name=\"telegram\">کانال بروزرسانی ها در تلگرام</string>\n    <string name=\"github\">کد منبع</string>\n    <string name=\"project\">پروژه</string>\n    <string name=\"app_desc\">ابزار جهانی پروکسی برای اندروید، نوشته شده در کاتلین.</string>\n    <string name=\"menu_configuration\">پیکربندی</string>\n    <string name=\"logcat_summary\">بسیار مفید وای واقعا عالی</string>\n    <string name=\"transproxy_mode\">حالت پراکسی شفاف</string>\n    <string name=\"require_transproxy\">فعالسازی پراکسی شفاف به عنوان ورودی</string>\n    <string name=\"port_local_dns\">پورت DNS محلی</string>\n    <string name=\"dns_hosts\">بازنویسی دستی دامنه ها</string>\n    <string name=\"fakedns_message\">احتمالا نرم افزارها بعد از متوقف کردن پراکسی نیاز به اجرای دوباره جهت دسترسی به اینترنت خواهند داشت</string>\n    <string name=\"enable_fakedns\">فعالسازی DNS جعلی</string>\n    <string name=\"dns_routing_message\">استفاده از DNS مستقیم برای مسیرهای خارج از پراکسی. احتمالا نشت DNS در این حالت وجود دارد</string>\n    <string name=\"enable_dns_routing\">فعالسازی مسیریابی DNS</string>\n    <string name=\"remote_dns\">DNS پروکسی</string>\n    <string name=\"direct_dns\">DNS مسیرهای مستقیم (خارج از پروکسی)</string>\n    <string name=\"cag_dns\">تنظیمات DNS</string>\n    <string name=\"connection_test_error_status_code\">شماره خطا: #%d</string>\n    <string name=\"connection_test_fail\">اینترنت در دسترس نیست</string>\n    <string name=\"connection_test_error\">ناموفق: %s</string>\n    <string name=\"connection_test_available_http\">موفق: اتصال HTTPS طی %d میلی ثانیه انجام شد</string>\n    <string name=\"connection_test_available\">موفقیت: ارتباط با HTTPS\\t%d میلی ثانیه طول کشید</string>\n    <string name=\"connection_test_testing\">در حال بررسی…</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"speed_detail\">پروکسی:\n\\n\\t%1$s↑ %2$s↓\n\\nمستقیم:\n\\n\\t%3$s↑ %4$s↓</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"allow_insecure_sum\">غیرفعال سازی بررسی گواهینامه امنیتی. در صورت فعال شدن این گزینه، تنظیمات فعلی امن نبوده و قابل شنود خواهند بود</string>\n    <string name=\"allow_insecure\">اجازه شکل گرفتن اتصال های غیرامن</string>\n    <string name=\"security_settings\">تنظیمات امنیتی</string>\n    <string name=\"show_direct_speed_sum\">نمایش سرعت مستقیم (داده هایی که طبق مسیریابی خارج از پراکسی مسیریابی می شوند) در اعلانات</string>\n    <string name=\"show_direct_speed\">نمایش سرعت اتصال مستقیم</string>\n    <string name=\"show_stop_sum\">در صورت عدم تمایل برای استفاده از دکمه نوار اعلان به عنوان سوییچ اتصال</string>\n    <string name=\"server_port\">پورت سرور</string>\n    <string name=\"server_address\">سرور</string>\n    <string name=\"profile_name\">نام پروفایل</string>\n    <string name=\"connection_test_url\">تست اتصال نشانی وب</string>\n    <string name=\"key_opt\">کلید (اختیاری)</string>\n    <string name=\"password_opt\">رمز عبور (اختیاری)</string>\n    <string name=\"username_opt\">نام کاربری (اختیاری)</string>\n    <string name=\"username\">نام کاربری</string>\n    <string name=\"password\">رمز عبور</string>\n    <string name=\"protocol_param\">پارامترهای پروتکل</string>\n    <string name=\"protocol\">پروتکل</string>\n    <string name=\"enc_method\">نوع رمزنگاری</string>\n    <string name=\"alter_id\">شماره تغییر</string>\n    <string name=\"uuid\">شناسه کاربر</string>\n    <string name=\"obfs_param\">پارامترهای Obfs</string>\n    <string name=\"obfs\">پروتکل Obfs</string>\n    <string name=\"header_type\">نوع سربرگ</string>\n    <string name=\"network\">شبکه</string>\n    <string name=\"security\">رمزنگاری لایه انتقال</string>\n    <string name=\"ws_host\">WebSocket میزبان</string>\n    <string name=\"ws_path\">مسیر WebSocket</string>\n    <string name=\"http_path\">مسیر HTTP</string>\n    <string name=\"http_host\">هاست HTTP</string>\n    <string name=\"grpc_service_name\">gRPC نام سرویس</string>\n    <string name=\"kcp_seed\">mKCP دانه(Seed)</string>\n    <string name=\"quic_key\">کلید QUIC</string>\n    <string name=\"quic_security\">امنیت QUIC</string>\n    <string name=\"only\">صرفا</string>\n    <string name=\"prefer\">ترجیحا</string>\n    <string name=\"ipv6\">مسیر IPv6</string>\n    <string name=\"edit_config\">ویرایش پیکربندی</string>\n    <string name=\"config_type\">نوع پیکربندی</string>\n    <string name=\"extra_headers\">سربرگ های اضافی</string>\n    <string name=\"encryption\">رمزنگاری</string>\n    <string name=\"early_data_header_name\">نام سربرگ داده های اولیه</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">زنجیره گواهینامه پین شده</string>\n    <string name=\"certificates\">گواهینامه ها</string>\n    <string name=\"alpn\">برنامه-مذاکره پروتکل لایه کاربردی</string>\n    <string name=\"sni\">نشانه نام سرور (SNI)</string>\n    <string name=\"tls\">استفاده از TLS</string>\n    <string name=\"force_resolve_sum\">هنگام به روزرسانی، تمام نام دامنه ها را به آدرس های IP تبدیل کنید. میزبان و SNI در صورت امکان به طور خودکار اضافه می شوند</string>\n    <string name=\"force_resolve\">حل و فصل کردن</string>\n    <string name=\"delete_group_prompt\">آیا مطمئن هستید که می خواهید این گروه را حذف کنید؟</string>\n    <string name=\"subscription_type\">نوع اشتراک</string>\n    <string name=\"group_type\">نوع گروه</string>\n    <string name=\"subscription_settings\">تنظیمات اشتراک</string>\n    <string name=\"subscription\">اشتراک‌ها</string>\n    <string name=\"group_settings\">تنظیمات گروه</string>\n    <string name=\"group_basic\">پایه</string>\n    <string name=\"ss_aead_provider\">ارائه دهنده Shadowsocks AEAD</string>\n    <string name=\"trojan_provider\">ارائه دهنده Trojan</string>\n    <string name=\"protocol_settings\">تنظیمات پروتکل</string>\n    <string name=\"bypass_lan_in_core_only_sum\">اگر لاین هات‌اسپات برای استفاده کاربرها از VPN کار نمی کند، این را امتحان کنید.</string>\n    <string name=\"bypass_lan_in_core_only\">بای‌پس LAN فقط در هسته</string>\n    <string name=\"action_from_link\">از اشتراک</string>\n    <string name=\"append_http_proxy_sum\">پروکسی HTTP مستقیماً از (مرورگر/ برخی از برنامه های پشتیبانی شده)، بدون عبور از دستگاه مجازی NIC استفاده می شود (اندروید ۱۰ و بالاتر)</string>\n    <string name=\"append_http_proxy\">اضافه کردن پروکسی HTTP به VPN</string>\n    <string name=\"connection_test_timeout\">منقضی شده</string>\n    <string name=\"connection_test_unreachable\">غیر قابل دسترسی</string>\n    <string name=\"connection_test_refused\">ارتباط رد شد</string>\n    <string name=\"connection_test_domain_not_found\">دامنه یافت نشد</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing در دسترس نیست</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing در دسترس نیست</string>\n    <string name=\"connection_test_clear_results\">پاک کردن نتایج تست</string>\n    <string name=\"connection_test\">تست اتصال</string>\n    <string name=\"clear_traffic_statistics\">پاک کردن آمار ترافیک</string>\n    <string name=\"always_show_address_sum\">همیشه آدرس سرور را روی کارت پیکربندی نمایش بده</string>\n    <string name=\"always_show_address\">همیشه آدرس را نشان بده</string>\n    <string name=\"unavailable\">در دسترس نیست</string>\n    <string name=\"standard\">استاندارد</string>\n    <string name=\"probe_interval\">فاصله مشاهده بالانس</string>\n    <string name=\"api_port\">پورت API</string>\n    <string name=\"random\">تصادفی</string>\n    <string name=\"list\">لیست</string>\n    <string name=\"enable_log_sum\">به منظور رفع اشکال</string>\n    <string name=\"enable_log\">فعال کردن گزارش</string>\n    <string name=\"auto\">خودکار</string>\n    <string name=\"disable\">غیرفعال کردن</string>\n    <string name=\"enable\">فعال کردن</string>\n    <string name=\"follow_system\">پیروی از سیستم</string>\n    <string name=\"night_mode\">حالت شب</string>\n    <string name=\"lines\">%d خطوط</string>\n    <string name=\"route_warn\">قبل از افزودن قوانین سفارشی فایروال، مطمئن شوید که مستندات را مطالعه کرده اید، در غیر این صورت ممکن است نتوانید به اینترنت متصل شوید.</string>\n    <string name=\"license\">مجوز</string>\n    <string name=\"restart\">سرویس پروکسی را دوباره راه اندازی کنید تا تغییرات اعمال شوند</string>\n    <string name=\"apply\">اجرا کردن</string>\n    <string name=\"no\">نه</string>\n    <string name=\"yes\">بله</string>\n    <string name=\"unsaved_changes_prompt\">تغییرات ذخیره نشد. آیا می خواهید ذخیره کنید؟</string>\n    <string name=\"ss_cat\">تنظیمات شادوساکس</string>\n    <string name=\"proxy_cat\">تنظیمات سرور</string>\n    <string name=\"plugin_auto_connect_unlock_only\">این افزونه ممکن است با اتصال خودکار کار نکند</string>\n    <string name=\"plugin_untrusted\">هشدار: به نظر نمی رسد که این افزونه از یک منبع معتبر شناخته شده باشد.</string>\n    <string name=\"plugin_unknown\">افزونه ناشناخته %s</string>\n    <string name=\"plugin_disabled\">غیرفعال شده</string>\n    <string name=\"plugin_configure\">پیکربندی…</string>\n    <string name=\"plugin\">افزونه</string>\n    <string name=\"ignore_battery_optimizations_sum\">برخی محدودیت ها را بردارید</string>\n    <string name=\"ignore_battery_optimizations\">نادیده گرفتن بهینه سازی باتری</string>\n    <string name=\"donate_info\">من عاشق پول هستم</string>\n    <string name=\"donate\">حمایت مالی</string>\n    <string name=\"deduplication\">حذف داده‏ های تکراری</string>\n    <string name=\"no_proxies_found_in_clipboard\">هیچ پروکسی در کلیپ بورد یافت نشد</string>\n    <string name=\"no_proxies_found_in_file\">هیچ پروکسی در پرونده یافت نشد</string>\n    <string name=\"no_proxies_found_in_subscription\">هیچ پروکسی در اشتراک یافت نشد</string>\n    <string name=\"no_proxies_found\">هیچ پروکسی در پیوند یافت نشد</string>\n    <string name=\"error_title\">خطا</string>\n    <string name=\"cleartext_http_warning\">ترافیک HTTP متن روشن ناامن است</string>\n    <string name=\"subscriptions\">اشتراک‌ها</string>\n    <string name=\"not_connected\">متصل نشد</string>\n    <string name=\"vpn_connected\">متصل شد ، برای بررسی اتصال ضربه بزنید</string>\n    <string name=\"connecting\">درحال اتصال…</string>\n    <string name=\"deprecated\">منسوخ شده</string>\n    <string name=\"insecure\">ناامن</string>\n    <string name=\"insecure_warn_sum\">ارائه شده توسط انجمن توسعه دهندگان Qv2ray</string>\n    <string name=\"insecure_warn\">مشاوره امنیتی</string>\n    <string name=\"undo\">بازگردانی</string>\n    <plurals name=\"added\">\n        <item quantity=\"one\">اضافه شد</item>\n        <item quantity=\"other\">%d پروکسی اضافه شد</item>\n    </plurals>\n    <plurals name=\"removed\">\n        <item quantity=\"one\">حذف شده</item>\n        <item quantity=\"other\">%d موارد حذف شده</item>\n    </plurals>\n    <string name=\"add_profile_methods_manual_settings\">تنظیمات دستی</string>\n    <string name=\"add_profile_methods_scan_qr_code\">اسکن کد QR</string>\n    <string name=\"share_qr_nfc\">کد QR</string>\n    <string name=\"delete_confirm_prompt\">آیا مطمئن هستید که می خواهید این نمایه را حذف کنید؟</string>\n    <string name=\"delete\">حذف</string>\n    <string name=\"profile_config\">پیکربندی نمایه</string>\n    <string name=\"file_manager_missing\">دستگاه شما فاقد انتخابگر پرونده استاندارد اندروید است، لطفاً یکی از آنها را نصب کنید، مانند Material Files.</string>\n    <string name=\"action_import_err\">وارد کردن انجام نشد.</string>\n    <string name=\"action_import_msg\">درون‌ریزی با موفقیت انجام شد!</string>\n    <string name=\"action_export_err\">صادرات انجام نشد.</string>\n    <string name=\"action_export_msg\">با موفقیت صادر شد!</string>\n    <string name=\"action_import_file\">وارد کردن از طریق فایل</string>\n    <string name=\"action_import\">وارد کردن از کلیپ بورد</string>\n    <string name=\"action_export\">برون‌بری</string>\n    <string name=\"action_export_clipboard\">خروجی به کلیپ بورد</string>\n    <string name=\"action_export_file\">خروجی در فایل</string>\n    <string name=\"action_scan_china_apps\">اسکن برنامه های چینی</string>\n    <string name=\"action_create_group\">گروه خالی</string>\n    <string name=\"clear_profiles\">پاک کردن</string>\n    <string name=\"circular_reference_sum\">مسیر نمی تواند خود را مهار کند.</string>\n    <string name=\"circular_reference\">مرجع دایره ای</string>\n    <string name=\"config_settings\">تنظیمات پیکربندی</string>\n    <string name=\"chain_settings\">تنظیمات زنجیره ای</string>\n    <string name=\"balancer_strategy\">استراتژی</string>\n    <string name=\"balancer_type\">نوع</string>\n    <string name=\"balancer_settings\">تنظیمات بالانس کننده</string>\n    <string name=\"balancer\">بالانس کننده</string>\n    <string name=\"custom_config\">پیکربندی سفارشی</string>\n    <string name=\"proxy_chain\">زنجیره پروکسی</string>\n    <string name=\"select_profile\">انتخاب پروفایل</string>\n    <string name=\"add_profile\">افزودن پروفایل</string>\n    <string name=\"share\">اشتراک گذاری</string>\n    <string name=\"edit\">ویرایش</string>\n    <string name=\"settings\">تنظیمات</string>\n    <string name=\"clipboard_empty\">کلیپ بورد خالی است</string>\n    <string name=\"connect\">اتصال</string>\n    <string name=\"profile_empty\">لطفاً یک نمایه انتخاب کنید</string>\n    <string name=\"reboot_required\">سرویس VPN شروع نشد. شاید لازم باشد دستگاه خود را دوباره راه اندازی کنید.</string>\n    <string name=\"vpn_permission_denied\">اجازه ایجاد سرویس VPN رد شد</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"stopping\">در حال خاموش شدن…</string>\n    <string name=\"stop\">متوقف کردن</string>\n    <string name=\"service_failed\">ناموفق:</string>\n    <string name=\"invalid_server\">نام سرور معتبر نیست</string>\n    <string name=\"forward_success\">پروکسی راه اندازی شد.</string>\n    <string name=\"service_proxy\">سرویس پروکسی</string>\n    <string name=\"service_vpn\">سرویس VPN</string>\n    <string name=\"direct_boot_aware_summary\">اطلاعات نمایه انتخابی شما کمتر محافظت می شود</string>\n    <string name=\"direct_boot_aware\">اجازه به تغییر وضعیت در قفل صفحه</string>\n    <string name=\"auto_connect_summary\">فعال کردن پروکسی در هنگام راه اندازی/به روز رسانی برنامه اگر قبل از آن در حال اجرا بود</string>\n    <string name=\"auto_connect\">اتصال خودکار</string>\n    <string name=\"show_system_apps\">نمایش برنامه‌های سیستمی</string>\n    <string name=\"bypass_apps\">بای‌پس(گذرگاه فرعی)</string>\n    <string name=\"clear_selections\">پاک کردن برگزیده ها</string>\n    <string name=\"invert_selections\">وارونه کردن برگزیده ها</string>\n    <string name=\"scanning\">درحال اسکن…</string>\n    <string name=\"search_apps\">جستجو…</string>\n    <string name=\"off\">خاموش</string>\n    <string name=\"on\">روشن</string>\n    <string name=\"proxied_apps_summary\">پیکربندی حالت VPN برای برنامه های انتخابی</string>\n    <string name=\"proxied_apps\">حالت VPN برای برنامه ها</string>\n    <string name=\"tcp_keep_alive_interval\">TCP نگه داشتن فاصله تحویل بسته فعال</string>\n    <string name=\"mux_for_all_sum\">اگر سرور از Mux پشتیبانی نکند، دیگر نمی توانیم به آن متصل شویم.</string>\n    <string name=\"mux_for_all\">فعالسازی Mux برای همه پروتکل های پشتیبانی شده</string>\n    <string name=\"mux_concurrency\">اتصالات همزمان Mux</string>\n    <string name=\"mux_sum\">تسهیم کننده Mux برای کاهش تأخیر TCP طراحی شده است، قادر به افزایش توان اتصال نیست. استفاده از Mux برای تماشای فیلم، بارگیری یا تست سرعت معمولاً خلاف قوانین تولید است</string>\n    <string name=\"enable_mux\">فعالسازی تسهیم کننده</string>\n    <string name=\"traffic_sniffing\">فعال کردن استشمام ترافیک</string>\n    <string name=\"domain_strategy\">استراتژی حل دامنه</string>\n    <string name=\"route_opt_block_ads\">مسدود کردن تبلیغات</string>\n    <string name=\"route_opt_bypass_lan\">بای‌پس LAN</string>\n    <string name=\"route_not_asset\">پرونده دارایی نیست: به استثنای .dat ، اما %s</string>\n    <string name=\"route_asset_updated\">به روز شده</string>\n    <string name=\"route_asset_no_update\">بدون بروزرسانی</string>\n    <string name=\"route_asset_status\">نسخه محلی: %s</string>\n    <string name=\"route_rules_official\">رسمی</string>\n    <string name=\"route_rules_provider\">قانون ارائه دهنده دارایی</string>\n    <string name=\"route_assets\">دارایی ها</string>\n    <string name=\"route_manage_assets\">مدیریت دارایی های مسیر</string>\n    <string name=\"route_reverse_redirect\">تغییر مسیر وارونه</string>\n    <string name=\"route_reverse\">وارونه کردن پروکسی</string>\n    <string name=\"route_reset\">تنظیم مجدد</string>\n    <string name=\"route_bypass_ip\">قانون IP برای %s</string>\n    <string name=\"route_bypass_domain\">قانون دامنه برای %s</string>\n    <string name=\"empty_route_notice\">قبل از ذخیره برخی از قوانین را تنظیم کنید</string>\n    <string name=\"empty_route\">مسیر خالی</string>\n    <string name=\"route_profile\">انتخاب نمایه…</string>\n    <string name=\"route_block\">مسدود کردن</string>\n    <string name=\"route_bypass\">بای‌پس(گذرگاه فرعی)</string>\n    <string name=\"route_proxy\">پروکسی</string>\n    <string name=\"route_name\">نام مسیر</string>\n    <string name=\"delete_route_prompt\">آیا مطمئن هستید که می خواهید این مسیر را حذف کنید؟</string>\n    <string name=\"route_add\">ایجاد مسیر</string>\n    <string name=\"menu_route\">مسیر</string>\n    <string name=\"metered_summary\">سیستم راهنمایی برای اشاره به اندازه گیری شبکه VPN</string>\n    <string name=\"metered\">راهنمای اندازه گیری</string>\n    <string name=\"apps_message\">%d برنامه ها</string>\n    <string name=\"select_apps\">انتخاب برنامه ها</string>\n    <string name=\"apps\">برنامه‌ها</string>\n    <string name=\"clear_profiles_message\">آیا مطمئن هستید که می خواهید این گروه را پاک کنید؟</string>\n    <string name=\"profile_import_message\">تأیید می کنید که می خواهید نمایه %s را وارد کنید؟</string>\n    <string name=\"profile_import\">ایمپورت کردن پروفایل</string>\n    <string name=\"subscription_import_message\">تأیید می کنید که می خواهید اشتراک را وارد كنید %s؟ اگر از یک منبع غیر معتمد می آیید، انجام این کار ممکن است منجر به فاش شدن IP و این رفتار شود.</string>\n    <string name=\"subscription_import\">وارد کردن اشتراک‌ها</string>\n    <string name=\"subscription_traffic\">%s استفاده شده / %s باقیمانده</string>\n    <string name=\"subscription_used\">%s استفاده شده</string>\n    <string name=\"group_filter_tags_nf\">هیچ برچسبی در این اشتراک وجود ندارد</string>\n    <string name=\"group_filter_groups_nf\">هیچ گروهی در این اشتراک وجود ندارد</string>\n    <string name=\"group_filter_ns\">برای استفاده از ویژگی فیلتر کردن، اشتراک OOCv1 لازم است</string>\n    <string name=\"group_filter_tags\">برچسب‌ها</string>\n    <string name=\"group_filter_groups\">گروه ها</string>\n    <string name=\"group_filter\">پالایش</string>\n    <string name=\"subscription_update_message\">درحال بروزرسانی %s …</string>\n    <string name=\"subscription_update\">به روز رسانی اشتراک</string>\n    <string name=\"service_subscription\">اشتراک خدمات به روز رسانی</string>\n    <string name=\"ooc_missing_protocol\">اشتراک نیاز به پشتیبانی از پروتکل %s دارد ، اما یافتن آن امکان پذیر نیست. نمایه های پشتیبانی نشده نادیده گرفته می شوند.</string>\n    <string name=\"ooc_warning\">هشدار</string>\n    <string name=\"update_subscription_warning\">پراکسی متصل نیست ، آیا مطمئن هستید که می خواهید به روزرسانی خود را ادامه دهید؟</string>\n    <string name=\"ooc_subscription_token_invalid\">رمز OOCv1 نامعتبر است</string>\n    <string name=\"download\">دانلود</string>\n    <string name=\"install_from_fdroid\">از F-Droid نصب کنید</string>\n    <string name=\"install_from_play_store\">از فروشگاه گوگل پلی نصب کنید</string>\n    <string name=\"action_download\">دانلود</string>\n    <string name=\"action_learn_more\">بیشتر بدانید</string>\n    <string name=\"profile_requiring_plugin\">نمایه %s نیاز به نصب پلاگین %s دارد، اما پیدا نشد.</string>\n    <string name=\"missing_plugin\">پلاگین موجود نیست</string>\n    <string name=\"confirm\">تایید</string>\n    <string name=\"subscription_user_agent\">عامل کاربر</string>\n    <string name=\"update_when_connected_only_sum\">از نشت آدرس IP جلوگیری کنید</string>\n    <string name=\"update_when_connected_only\">فقط هنگام اتصال به روز کنید</string>\n    <string name=\"auto_update_delay\">تأخیر به روزرسانی خودکار (در چند دقیقه)</string>\n    <string name=\"auto_update\">بروزرسانی خودکار</string>\n    <string name=\"update_settings\">بروزرسانی تنظیمات</string>\n    <string name=\"raw\">خام</string>\n    <string name=\"deduplication_sum\">هنگام به روزرسانی، پیکربندی های تکراری را حذف کنید</string>\n    <string name=\"force_vmess_aead_sum\">بازنویسی پیکربندی VMess با روش تأیید ناامن به AEAD (تغییر 0)</string>\n    <string name=\"force_vmess_aead\">اجبار VMess AEAD</string>\n    <string name=\"hysteria_download_mbps\">حداکثر سرعت بارگیری (در مگابیت بر ثانیه)</string>\n    <string name=\"hysteria_upload_mbps\">حداکثر سرعت بارگذاری (در مگابیت بر ثانیه)</string>\n    <string name=\"hysteria_auth_payload\">ظرفیت بار احراز هویت</string>\n    <string name=\"hysteria_auth_type\">نوع احراز هویت</string>\n    <string name=\"hysteria_obfs\">رمز عبور مبهم</string>\n    <string name=\"experimental_no_termination_signal\">هنگام انتقال از طریق VMess، سیگنال ختم دو طرفه اتصال را برای اتصال TCP ارسال نکنید. این باعث خرابی برخی از برنامه ها می شود.</string>\n    <string name=\"experimental_authenticated_length\">طول هر بخش محموله دیگر قابل انعطاف نیست. این آزمایش نیاز به سرور دارد و سرویس گیرنده از نسخه مشابه v2ray-core استفاده می کند. انتظار می رود بروزرسانی های فوری بیشتری در مورد این آزمایش ارائه شود.</string>\n    <string name=\"experimental_settings\">آزمایشی</string>\n    <string name=\"route_need_fds\">قانون مسیریابی %s متکی به فعال بودن سرویس تشخیص پیش زمینه است، اما فعال نیست.</string>\n    <string name=\"route_need_vpn\">قانون مسیریابی %s متکی به در دسترس بودن VPN است، بنابراین نادیده گرفته می شود.</string>\n    <string name=\"background\">پس‌زمینه</string>\n    <string name=\"foreground\">پیش زمینه</string>\n    <string name=\"foreground_status\">وضعیت پیش زمینه</string>\n    <string name=\"foreground_detector_summary\">این سرویس از قوانین مسیریابی بر اساس وضعیت پیش زمینه برنامه ها پشتیبانی می کند. اگر شروع نشده باشد، نتیجه همیشه در پس زمینه است.</string>\n    <string name=\"foreground_detector\">آشکارساز پیش زمینه</string>\n    <string name=\"route_for\">قانون برای %s</string>\n    <string name=\"app_no_launcher\">برنامه فاقد رابط کاربری است.</string>\n    <string name=\"copy_failed\">کپی نشد.</string>\n    <string name=\"copy_success\">با موفقیت کپی شد!</string>\n    <string name=\"create_rule\">ایجاد قانون</string>\n    <string name=\"open_market\">بازکردن مارکت</string>\n    <string name=\"open_settings\">بازکردن تنظیمات</string>\n    <string name=\"open_app\">باز کردن برنامه</string>\n    <string name=\"copy_package_name\">کپی نام بسته</string>\n    <string name=\"copy_label\">کپی نام</string>\n    <string name=\"traffic_holder\">باز کردن VPN برای ثبت آمار ترافیک</string>\n    <string name=\"udp_connections\">%d اتصالات UDP</string>\n    <string name=\"tcp_connections\">%d اتصالات TCP</string>\n    <string name=\"traffic_stats\">آمار</string>\n    <string name=\"traffic_active\">فعال</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (پلاگین اندروید شادوساکس)</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Obfs ساده (پلاگین اندروید شادوساکس)</string>\n    <string name=\"group_order_origin\">بر اساس سرآغاز</string>\n    <string name=\"group_order\">چیدمان</string>\n    <string name=\"group_order_by_delay\">بر اساس تأخیر</string>\n    <string name=\"group_order_by_name\">بر اساس نام</string>\n    <string name=\"hysteria_disable_mtu_discovery\">غیر فعال کردن مسیر کشف MTU</string>\n    <string name=\"hysteria_connection_receive_window\">پنجره دریافت اتصال سریع</string>\n    <string name=\"hysteria_stream_receive_window\">پنجره دریافت جریان QUIC</string>\n    <string name=\"ss_stream_provider\">ارائه دهنده جریان شادوساکس</string>\n    <string name=\"menu_traffic\">ترافیک</string>\n    <string name=\"no_statistics\">هنوز آماری در دسترس نیست</string>\n    <string name=\"traffic_statistics_summary\">ممکن است مصرف باتری را افزایش دهد</string>\n    <string name=\"clear_logcat\">پاکسازی گزارش عملکرد</string>\n    <string name=\"menu_log\">گزارش عملکرد</string>\n    <string name=\"menu_tools\">ابزارها</string>\n    <string name=\"translate_platform\">پلتفرم ترجمه</string>\n    <string name=\"ssh_private_key_passphrase\">عبارت عبور کلید خصوصی</string>\n    <string name=\"ssh_private_key\">کلید خصوصی</string>\n    <string name=\"ssh_public_key\">کلید عمومی</string>\n    <string name=\"ssh_auth_type_none\">هیچ‌کدام</string>\n    <string name=\"wireguard_psk\">کلید از پیش اشتراکی همتا</string>\n    <string name=\"wireguard_public_key\">کلید عمومی همتا</string>\n    <string name=\"wireguard_local_address\">آدرس محلی</string>\n    <string name=\"destination_override\">نادیده گرفتن مقصد</string>\n    <string name=\"resolve_destination\">حل مقصد</string>\n    <string name=\"profile_traffic_statistics\">پروفایل آمار ترافیک</string>\n    <string name=\"app_statistics_disabled\">آمار ترافیک برنامه غیرفعال شده است</string>\n    <string name=\"warp_generate\">ایجاد کانفیگ</string>\n    <string name=\"generating\">در حال تولید…</string>\n    <string name=\"tun_implementation\">پیاده سازی TUN</string>\n    <string name=\"destination_override_summary\">از دامنه استشمام شده برای بازنویسی آدرس مقصد استفاده کنید، نه فقط برای مسیریابی</string>\n    <string name=\"resolve_destination_summary\">اگر آدرس مقصد یک دامنه باشد، پس از آن بر اساس استراتژی IPv6 منتقل می شود.</string>\n    <string name=\"enable_pcap\">فعال کردن Pcap</string>\n    <string name=\"pcap_summary\">ذخیره ترافیک در فایل .pcap</string>\n    <string name=\"group_filter_owners\">مالکان</string>\n    <string name=\"group_filter_owners_nf\">هیچ مالکی در این اشتراک وجود ندارد</string>\n    <string name=\"plugin_exists_but_on_shit_system\">نمایه %s به پلاگین %s نیاز دارد، اما فروشنده تجهیزات اختصاصی شما (معمولاً غول های بزرگ نظارت کننده و سازنده بدافزارها) اندروید شما را دستکاری کرده و پلاگین را غیرقابل استفاده کرده است.</string>\n    <string name=\"app_traffic_statistics\">آمار ترافیک برنامه</string>\n    <string name=\"profile_traffic_statistics_summary\">در صورت غیرفعال بودن، ترافیک استفاده شده محاسبه نمی شود</string>\n    <string name=\"warp_license\">CloudFlare Wrap یک ارائه دهنده رایگان WireGuard VPN است. با استفاده از آن، شما با TOS موافقت می کنید: https://www.cloudflare.com/application/terms/</string>\n    <string name=\"pcap_notice\">فایل های Pcap در %s ذخیره می شوند</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"action_import_err\">Échec de l\\'importation.</string>\n    <string name=\"delete_confirm_prompt\">Êtes-vous sûr(e) de vouloir retirer ce profil \\?</string>\n    <string name=\"delete\">Retirer</string>\n    <string name=\"add_profile_methods_scan_qr_code\">Scannez le code QR</string>\n    <string name=\"share_qr_nfc\">Code QR</string>\n    <string name=\"deprecated\">Obsolète</string>\n    <string name=\"insecure\">Non sûr</string>\n    <string name=\"donate\">Faire un don</string>\n    <string name=\"plugin\">Greffon</string>\n    <string name=\"ignore_battery_optimizations_sum\">Retirer certaines restrictions</string>\n    <string name=\"ignore_battery_optimizations\">Ignorer l\\'optimisation de la batterie</string>\n    <string name=\"donate_info\">J\\'adore l\\'argent</string>\n    <string name=\"apply\">Appliquer</string>\n    <string name=\"no\">Non</string>\n    <string name=\"yes\">Oui</string>\n    <string name=\"license\">Licence</string>\n    <string name=\"follow_system\">Selon le système</string>\n    <string name=\"night_mode\">Mode nuit</string>\n    <string name=\"lines\">%d lignes</string>\n    <string name=\"random\">Aléatoire</string>\n    <string name=\"list\">Liste</string>\n    <string name=\"enable_log_sum\">Pour le débogage</string>\n    <string name=\"enable_log\">Activer la journalisation</string>\n    <string name=\"delete_group_prompt\">Êtes-vous sûr(e) de vouloir supprimer ce groupe \\?</string>\n    <string name=\"subscription\">Abonnement</string>\n    <string name=\"profile_requiring_plugin\">%s nécessite l\\'installation du greffon %s, mais il n\\'a pas été trouvé.</string>\n    <string name=\"missing_plugin\">Greffon manquant</string>\n    <string name=\"confirm\">Confirmer</string>\n    <string name=\"action_download\">TÉLÉCHARGER</string>\n    <string name=\"action_learn_more\">EN SAVOIR PLUS</string>\n    <string name=\"download\">Télécharger</string>\n    <string name=\"apps_message\">%d applications</string>\n    <string name=\"select_apps\">Sélectionnez les applications</string>\n    <string name=\"apps\">Applications</string>\n    <string name=\"clear_profiles_message\">Êtes-vous sûr(e) de vouloir supprimer ce groupe \\?</string>\n    <string name=\"profile_import_message\">Confirmez-vous que vous voulez importer le profil %s \\?</string>\n    <string name=\"group_filter_tags_nf\">Aucune étiquette dans cet abonnement</string>\n    <string name=\"group_filter_tags\">Étiquettes</string>\n    <string name=\"group_filter_groups\">Groupes</string>\n    <string name=\"general_settings\">Paramètres de l\\'appli</string>\n    <string name=\"cag_route\">Paramètres d\\'itinéraire</string>\n    <string name=\"service_mode\">Mode service</string>\n    <string name=\"group_delete_confirm_prompt\">Êtes-vous sûr(e) de vouloir supprimer ce groupe \\?</string>\n    <string name=\"group_diff\">Diff (%s)</string>\n    <string name=\"group_show_diff\">Diff</string>\n    <string name=\"group_updated\">%s : %d proxys mis à jour</string>\n    <string name=\"group_no_difference\">%s : aucune différence</string>\n    <string name=\"group_status_proxies_subscription\">%d proxys | Mis à jour le %s</string>\n    <string name=\"group_status_proxies\">%d proxys</string>\n    <string name=\"group_status_empty_subscription\">Pas encore mis à jour</string>\n    <string name=\"group_status_empty\">Groupe vide</string>\n    <string name=\"group_edit_subscription\">Modifier l\\'abonnement</string>\n    <string name=\"group_edit\">Modifier le groupe</string>\n    <string name=\"group_create_subscription\">Créer un abonnement</string>\n    <string name=\"group_create\">Créer un groupe</string>\n    <string name=\"group_name_required\">Nom de groupe requis</string>\n    <string name=\"group_subscription_link\">Lien d’abonnement</string>\n    <string name=\"group_update\">Mettre à jour</string>\n    <string name=\"group_name\">Nom du groupe</string>\n    <string name=\"quick_toggle\">Basculer</string>\n    <string name=\"group_default\">Dégroupé</string>\n    <string name=\"document\">Document</string>\n    <string name=\"theme\">Thème</string>\n    <string name=\"menu_about\">À propos</string>\n    <string name=\"menu_group\">Groupe</string>\n    <string name=\"menu_configuration\">Configuration</string>\n    <string name=\"logcat\">Exporter les informations de débogage</string>\n    <string name=\"version_x\">Version (%s)</string>\n    <string name=\"app_version\">Version</string>\n    <string name=\"oss_licenses\">Licences à code source ouvert</string>\n    <string name=\"telegram\">Canal de mise à jour Telegram</string>\n    <string name=\"github\">Code source</string>\n    <string name=\"project\">Projet</string>\n    <string name=\"app_desc\">La chaîne d\\'outils proxy universelle pour Android, écrite en Kotlin.</string>\n    <string name=\"hysteria_upload_mbps\">Vitesse maximale de téléversement (en Mb/s)</string>\n    <string name=\"hysteria_download_mbps\">Vitesse de téléchargement maximale (en Mb/s)</string>\n    <string name=\"group_deleted\">Supprimé :\n\\n%s</string>\n    <string name=\"group_changed\">Mis à jour :\n\\n%s</string>\n    <string name=\"group_added\">Ajouté :\n\\n%s</string>\n    <string name=\"tile_title\">Commutateur</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode_proxy\">Agent seulement</string>\n    <string name=\"group_duplicate\">Reproduction:\n\\n%s</string>\n    <string name=\"logcat_summary\">Ça marche</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"port_http\">Proksi Port HTTP</string>\n    <string name=\"port_proxy\">Proksi Port SOCKS5</string>\n    <string name=\"group_status_proxies_subscription\">%d Proksi | Diperbarui pada %s</string>\n    <string name=\"group_status_proxies\">%d Proksi</string>\n    <string name=\"protocol_param\">Param Protokol</string>\n    <string name=\"protocol\">Protokol</string>\n    <string name=\"enc_method\">Metode Enkrip</string>\n    <string name=\"password\">Kata sandi</string>\n    <string name=\"key_opt\">Kunci (Opsional)</string>\n    <string name=\"password_opt\">Kata sandi (Opsional)</string>\n    <string name=\"username_opt\">Nama pengguna (Opsional)</string>\n    <string name=\"username\">Nama pengguna</string>\n    <string name=\"server_port\">Remot Port</string>\n    <string name=\"server_address\">Server</string>\n    <string name=\"profile_name\">Nama Profil</string>\n    <string name=\"connection_test_url\">URL Tes Koneksi</string>\n    <string name=\"transproxy_mode\">Mode Transproksi</string>\n    <string name=\"require_transproxy\">Aktifkan Inbound Transproksi</string>\n    <string name=\"port_local_dns\">Port DNS lokal</string>\n    <string name=\"dns_hosts\">Tulis ulang DNS</string>\n    <string name=\"fakedns_message\">Dapat menyebabkan aplikasi lain harus dimulai ulang untuk menyambung kembali ke jaringan setelah proksi dihentikan</string>\n    <string name=\"enable_fakedns\">Aktifkan FakeDNS</string>\n    <string name=\"dns_routing_message\">Selesaikan domain di rute bypass dengan Direct DNS. Waspadai potensi kebocoran DNS</string>\n    <string name=\"enable_dns_routing\">Aktifkan DNS Routing</string>\n    <string name=\"direct_dns\">Direk DNS</string>\n    <string name=\"remote_dns\">Remot DNS</string>\n    <string name=\"cag_dns\">Pengaturan DNS</string>\n    <string name=\"connection_test_error_status_code\">Kode galat: #%d</string>\n    <string name=\"connection_test_fail\">Internet tidak tersedia</string>\n    <string name=\"connection_test_error\">Gagal: %s</string>\n    <string name=\"connection_test_available_http\">Sukses: handshake HTTP butuh %dms</string>\n    <string name=\"connection_test_available\">Sukses: handshake HTTPS butuh %dms</string>\n    <string name=\"connection_test_testing\">Mengetes…</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"speed_detail\">Proksi： %1$s↑ %2$s↓\n\\nDirek： %3$s↑ %4$s↓</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"allow_insecure_sum\">Nonaktifkan pengecekan sertifikat. Saat diaktifkan, konfigurasi ini seaman teks biasa</string>\n    <string name=\"allow_insecure\">Izinkan Koneksi Tidak Aman</string>\n    <string name=\"security_settings\">Pengaturan Keamanan</string>\n    <string name=\"show_direct_speed_sum\">Tampilkan lalu lintas kecepatan koneksi tanpa proksi di notifikasi</string>\n    <string name=\"show_direct_speed\">Tampilkan Kecepatan Direk</string>\n    <string name=\"show_stop_sum\">Jika Anda tidak ingin menggunakan Tombol Ubin sebagai pengalih</string>\n    <string name=\"show_stop\">Tampilkan Tombol Henti</string>\n    <string name=\"cag_misc\">Pengaturan Lainnya</string>\n    <string name=\"speed_interval\">Interval Pembaruan Notifikasi Kecepatan</string>\n    <string name=\"ws_browser_forwarding_sum\">Teruskan WebSocket yang sesuai melalui browser.</string>\n    <string name=\"ws_browser_forwarding\">Penerusan Peramban</string>\n    <string name=\"cag_ws\">Pengaturan WebSocket</string>\n    <string name=\"require_http\">Aktifkan HTTP inbound</string>\n    <string name=\"general_settings\">Pengaturan Aplikasi</string>\n    <string name=\"inbound_settings\">Pengaturan Inbound</string>\n    <string name=\"allow_access_sum\">Ikat server masuk ke 0.0.0.0</string>\n    <string name=\"allow_access\">Izinkan koneksi dari LAN</string>\n    <string name=\"cag_route\">Pengaturan Rute</string>\n    <string name=\"port_transproxy\">Port Transproxy</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode_proxy\">Hanya proxy</string>\n    <string name=\"service_mode\">Mode Layanan</string>\n    <string name=\"group_duplicate\">Duplikasi:\n\\n%s</string>\n    <string name=\"group_deleted\">Dihapus:\n\\n%s</string>\n    <string name=\"group_changed\">Diperbarui:\n\\n%s</string>\n    <string name=\"group_added\">Ditambahkan:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">Anda yakin ingin menghapus grup ini\\?</string>\n    <string name=\"group_diff\">Diff (%s)</string>\n    <string name=\"group_show_diff\">Diff</string>\n    <string name=\"group_updated\">%s: Diperbarui %d proxy</string>\n    <string name=\"group_no_difference\">%s: Tidak ada perbedaan</string>\n    <string name=\"group_status_empty\">Grup kosong</string>\n    <string name=\"group_status_empty_subscription\">Belum diperbarui</string>\n    <string name=\"group_edit_subscription\">Ubah langganan</string>\n    <string name=\"group_edit\">Ubah grup</string>\n    <string name=\"group_create_subscription\">Buat langganan</string>\n    <string name=\"group_create\">Buat grup</string>\n    <string name=\"group_name_required\">Nama grup diperlukan</string>\n    <string name=\"group_subscription_link\">Tautan Langganan</string>\n    <string name=\"group_update\">Perbarui</string>\n    <string name=\"group_name\">Nama grup</string>\n    <string name=\"tile_title\">Pengalih</string>\n    <string name=\"quick_toggle\">Beralih</string>\n    <string name=\"group_default\">Tidak bergrup</string>\n    <string name=\"document\">Dokumen</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"menu_about\">Tentang</string>\n    <string name=\"menu_group\">Grup</string>\n    <string name=\"menu_configuration\">Konfigurasi</string>\n    <string name=\"logcat_summary\">Sangat bermanfaat sekali</string>\n    <string name=\"logcat\">Ekspor informasi debug</string>\n    <string name=\"version_x\">Versi (%s)</string>\n    <string name=\"app_version\">Versi</string>\n    <string name=\"oss_licenses\">Lisensi sumber terbuka</string>\n    <string name=\"telegram\">Channel Telegram</string>\n    <string name=\"github\">Kode sumber</string>\n    <string name=\"project\">Proyek</string>\n    <string name=\"app_desc\">Peranti proxy universal untuk Android, ditulis dalam Kotlin.</string>\n    <string name=\"ssh_auth_type_none\">Kosong</string>\n    <string name=\"no_statistics\">Belum ada statistik</string>\n    <string name=\"menu_tools\">Alat</string>\n    <string name=\"ssh_private_key\">Kunci Privat</string>\n    <string name=\"ssh_public_key\">Kunci Publik</string>\n    <string name=\"profile_empty\">Silakan pilih profil</string>\n    <string name=\"auto_connect\">Sambungkan Otomatis</string>\n    <string name=\"encryption\">Enkripsi</string>\n    <string name=\"kcp_seed\">mKCP Seed</string>\n    <string name=\"select_profile\">Pilih Profil</string>\n    <string name=\"add_profile\">Tambah Profil</string>\n    <string name=\"share\">Bagikan</string>\n    <string name=\"settings\">Pengaturan</string>\n    <string name=\"clipboard_empty\">Papan klip kosong</string>\n    <string name=\"connect\">Sambungkan</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"stopping\">Mematikan…</string>\n    <string name=\"stop\">Berhenti</string>\n    <string name=\"service_failed\">Gagal:</string>\n    <string name=\"invalid_server\">Nama server tidak valid</string>\n    <string name=\"forward_success\">Proksi menyala.</string>\n    <string name=\"service_proxy\">Layanan Proksi</string>\n    <string name=\"service_vpn\">Layanan VPN</string>\n    <string name=\"scanning\">Memindai…</string>\n    <string name=\"search_apps\">Cari…</string>\n    <string name=\"route_name\">Nama rute</string>\n    <string name=\"route_manage_assets\">Atur rute aset</string>\n    <string name=\"route_assets\">Aset</string>\n    <string name=\"enable_mux\">Aktifkan Multiplexer</string>\n    <string name=\"traffic_sniffing\">Aktifkan Traffic Sniffing</string>\n    <string name=\"off\">Mati</string>\n    <string name=\"on\">Nyala</string>\n    <string name=\"route_opt_block_ads\">Blokir ADs</string>\n    <string name=\"route_asset_updated\">Diperbarui</string>\n    <string name=\"route_asset_no_update\">Tidak ada pembaruan</string>\n    <string name=\"route_asset_status\">Versi lokal: %s</string>\n    <string name=\"route_rules_official\">Resmi</string>\n    <string name=\"route_reverse\">Reverse Proksi</string>\n    <string name=\"route_reset\">Setel ulang</string>\n    <string name=\"empty_route\">Rute Kosong</string>\n    <string name=\"route_profile\">Pilih Profil…</string>\n    <string name=\"route_block\">Blokir</string>\n    <string name=\"route_bypass\">Bypass</string>\n    <string name=\"route_proxy\">Proksi</string>\n    <string name=\"only\">Hanya</string>\n    <string name=\"prefer\">Lebih menyukai</string>\n    <string name=\"ipv6\">Rute IPv6</string>\n    <string name=\"edit_config\">Sunting Konfigurasi</string>\n    <string name=\"certificates\">Sertifikat</string>\n    <string name=\"tls\">Pakai TLS</string>\n    <string name=\"network\">Jaringan</string>\n    <string name=\"uuid\">ID Pengguna</string>\n    <string name=\"obfs_param\">Obfs Param</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"menu_traffic\">Trafik</string>\n    <string name=\"circular_reference\">Referensi melingkar</string>\n    <string name=\"config_settings\">Pengaturan Konfigurasi</string>\n    <string name=\"chain_settings\">Pengaturan Rantai</string>\n    <string name=\"balancer_strategy\">Strategi</string>\n    <string name=\"balancer_type\">Jenis</string>\n    <string name=\"balancer_settings\">Pengaturan Penyeimbang</string>\n    <string name=\"balancer\">Pengimbang</string>\n    <string name=\"custom_config\">Konfigurasi Kustom</string>\n    <string name=\"proxy_chain\">Rantai Proksi</string>\n    <string name=\"edit\">Sunting</string>\n    <string name=\"reboot_required\">Gagal memulai layanan VPN. Anda mungkin perlu me-reboot perangkat Anda.</string>\n    <string name=\"vpn_permission_denied\">Izin ditolak untuk membuat layanan VPN</string>\n    <string name=\"direct_boot_aware_summary\">Informasi profil yang Anda pilih akan kurang terlindungi</string>\n    <string name=\"direct_boot_aware\">Izinkan Beralih di Layar Kunci</string>\n    <string name=\"auto_connect_summary\">Aktifkan proxy pada pembaruan startup/aplikasi jika sudah berjalan sebelumnya</string>\n    <string name=\"show_system_apps\">Tampilkan aplikasi sistem</string>\n    <string name=\"bypass_apps\">Jalan pintas</string>\n    <string name=\"clear_selections\">Hapus pilihan</string>\n    <string name=\"invert_selections\">Balikkan pilihan</string>\n    <string name=\"proxied_apps_summary\">Konfigurasikan mode VPN untuk aplikasi yang dipilih</string>\n    <string name=\"proxied_apps\">Modus aplikasi VPN</string>\n    <string name=\"tcp_keep_alive_interval\">TCP menjaga interval pengiriman paket aktif</string>\n    <string name=\"mux_for_all_sum\">Jika server tidak mendukung Mux, kami tidak akan dapat menyambungkannya.</string>\n    <string name=\"mux_for_all\">Aktifkan Mux untuk semua kemungkinan protokol yang didukung</string>\n    <string name=\"mux_concurrency\">Koneksi Mux Concurrent</string>\n    <string name=\"mux_sum\">Mux dirancang untuk mengurangi latensi handshake TCP, bukan untuk meningkatkan throughput koneksi. Menggunakan Mux untuk menonton video, mengunduh, atau uji kecepatan biasanya kontra produktif</string>\n    <string name=\"domain_strategy\">Strategi Resolusi Domain</string>\n    <string name=\"route_opt_bypass_lan\">Lewati LAN</string>\n    <string name=\"route_not_asset\">Bukan file aset: kecuali .dat, tapi %s</string>\n    <string name=\"route_rules_provider\">Penyedia Aset Aturan</string>\n    <string name=\"route_reverse_redirect\">Pengalihan Terbalik</string>\n    <string name=\"route_bypass_domain\">Aturan domain untuk %s</string>\n    <string name=\"empty_route_notice\">Tetapkan beberapa aturan sebelum menyimpan</string>\n    <string name=\"delete_route_prompt\">Apakah anda yakin ingin menghapus rute ini\\?</string>\n    <string name=\"route_add\">Buat Rute</string>\n    <string name=\"menu_route\">Rute</string>\n    <string name=\"metered_summary\">Sistem petunjuk untuk memperlakukan VPN sebagai terukur</string>\n    <string name=\"metered\">Petunjuk Terukur</string>\n    <string name=\"config_type\">Jenis Konfigurasi</string>\n    <string name=\"extra_headers\">Header Ekstra</string>\n    <string name=\"early_data_header_name\">Nama Header Data Awal</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Rantai Sertifikat yang Disematkan</string>\n    <string name=\"alpn\">Negosiasi Protokol Lapisan Aplikasi</string>\n    <string name=\"sni\">Indikasi Nama Server</string>\n    <string name=\"grpc_service_name\">Nama Layanan gRPC</string>\n    <string name=\"quic_key\">KUNCI Quic</string>\n    <string name=\"quic_security\">Keamanan QUIC</string>\n    <string name=\"http_path\">Jalur HTTP</string>\n    <string name=\"http_host\">HTTP Host</string>\n    <string name=\"ws_path\">Jalur Soket Web</string>\n    <string name=\"ws_host\">Host Soket Web</string>\n    <string name=\"header_type\">Jenis Tajuk</string>\n    <string name=\"security\">Enkripsi lapisan transportasi</string>\n    <string name=\"alter_id\">Ubah ID</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"logcat\">Esporta informazioni di debug</string>\n    <string name=\"version_x\">Versione (%s)</string>\n    <string name=\"app_version\">Versione</string>\n    <string name=\"oss_licenses\">Licenze open source</string>\n    <string name=\"telegram\">Canale di aggiornamento di Telegram</string>\n    <string name=\"github\">Codice sorgente</string>\n    <string name=\"project\">Progetto</string>\n    <string name=\"app_desc\">La toolchain proxy universale per Android, scritta in Kotlin.</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"allow_access_sum\">インバウンドサーバーを0.0.0.0にバインドします</string>\n    <string name=\"allow_access\">LANからの接続を許可する</string>\n    <string name=\"cag_route\">ルート設定</string>\n    <string name=\"port_transproxy\">トランスプロキシポート</string>\n    <string name=\"port_http\">HTTPプロキシポート</string>\n    <string name=\"port_proxy\">SOCKS5プロキシポート</string>\n    <string name=\"service_mode_proxy\">プロキシのみ</string>\n    <string name=\"service_mode\">サービスモード</string>\n    <string name=\"group_duplicate\">複製：\n\\n%s</string>\n    <string name=\"group_deleted\">削除しました：\n\\n%s</string>\n    <string name=\"group_changed\">更新しました：\n\\n%s</string>\n    <string name=\"group_added\">追加した：\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">このグループを削除してもよろしいですか？</string>\n    <string name=\"group_diff\">差分 (%s)</string>\n    <string name=\"group_show_diff\">差分</string>\n    <string name=\"group_updated\">%s：更新された %d プロキシ</string>\n    <string name=\"group_no_difference\">%s：違いなし</string>\n    <string name=\"group_status_proxies_subscription\">%d プロキシ| %s で更新</string>\n    <string name=\"group_status_proxies\">%d プロキシ</string>\n    <string name=\"group_status_empty_subscription\">まだ更新されていません</string>\n    <string name=\"group_status_empty\">空のグループ</string>\n    <string name=\"group_edit_subscription\">サブスクリプションの編集</string>\n    <string name=\"group_edit\">グループを編集</string>\n    <string name=\"group_create_subscription\">サブスクリプションを作成する</string>\n    <string name=\"group_create\">グループを作成します</string>\n    <string name=\"group_name_required\">グループ名が必要です</string>\n    <string name=\"group_subscription_link\">サブスクリプションリンク</string>\n    <string name=\"group_update\">アップデート</string>\n    <string name=\"group_name\">グループ名</string>\n    <string name=\"tile_title\">スイッチャー</string>\n    <string name=\"quick_toggle\">トグル</string>\n    <string name=\"group_default\">グループ化されていない</string>\n    <string name=\"document\">ドキュメント（英語）</string>\n    <string name=\"theme\">テーマ</string>\n    <string name=\"menu_about\">このアプリについて</string>\n    <string name=\"menu_group\">グループ</string>\n    <string name=\"menu_configuration\">設定</string>\n    <string name=\"logcat_summary\">そのような便利な非常にすごい</string>\n    <string name=\"logcat\">デバッグ情報のエクスポート</string>\n    <string name=\"version_x\">バージョン (%s)</string>\n    <string name=\"app_version\">バージョン</string>\n    <string name=\"oss_licenses\">オープンソースライセンス</string>\n    <string name=\"telegram\">Telegram 更新チャンネル</string>\n    <string name=\"github\">ソースコード</string>\n    <string name=\"project\">プロジェクト</string>\n    <string name=\"app_desc\">Kotlinで記述されたAndroid用のユニバーサルプロキシツールチェーン。</string>\n    <string name=\"cag_dns\">DNS設定</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"general_settings\">アブリ設定</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"menu_route\">ルート</string>\n    <string name=\"uuid\">ユーザーID</string>\n    <string name=\"obfs_param\">Obfsパラメータ</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"protocol_param\">プロトコルパラメータ</string>\n    <string name=\"protocol\">プロトコル</string>\n    <string name=\"enc_method\">暗号化方式</string>\n    <string name=\"password\">パスワード</string>\n    <string name=\"password_opt\">パスワード（選択可能）</string>\n    <string name=\"username_opt\">ユーザー名（選択可能）</string>\n    <string name=\"username\">ユーザー名</string>\n    <string name=\"transproxy_mode\">透過プロキシモード</string>\n    <string name=\"server_port\">リモートポート</string>\n    <string name=\"server_address\">サーバー</string>\n    <string name=\"profile_name\">プロフィール名</string>\n    <string name=\"connection_test_url\">テスト用リンクを接続</string>\n    <string name=\"port_local_dns\">ローカルDNSポート</string>\n    <string name=\"dns_hosts\">ドメインの書き換え</string>\n    <string name=\"fakedns_message\">プロキシが停止した後、ネットワークに再接続するために他のアプリケーションを再起動する必要がある場合があります。</string>\n    <string name=\"enable_fakedns\">FakeDNSを有効化</string>\n    <string name=\"enable_dns_routing\">DNSルーティングを有効化</string>\n    <string name=\"direct_dns\">ダイレクトDNS</string>\n    <string name=\"remote_dns\">リモートDNS</string>\n    <string name=\"connection_test_error_status_code\">エラーコード: #%d</string>\n    <string name=\"connection_test_fail\">インターネットは利用できません</string>\n    <string name=\"connection_test_error\">失敗: %s</string>\n    <string name=\"connection_test_testing\">テスト中…</string>\n    <string name=\"speed_detail\">プロキシ： %1$s↑ %2$s↓\n\\nダイレクト： %3$s↑ %4$s↓</string>\n    <string name=\"security_settings\">セキュリティ設定</string>\n    <string name=\"show_direct_speed\">ダイレクト接続のスピードを表示</string>\n    <string name=\"show_stop_sum\">クィックタイトルを使えなくなりたいの場合</string>\n    <string name=\"show_stop\">中止バタンを表示</string>\n    <string name=\"ws_browser_forwarding\">ブラウザへ転送</string>\n    <string name=\"cag_ws\">WebSocket 設定</string>\n    <string name=\"require_http\">HTTPでインバウンドを開く</string>\n    <string name=\"inbound_settings\">インバウンド設定</string>\n    <string name=\"menu_traffic\">トラフィック</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"logcat_summary\">너무 유용하다 와우</string>\n    <string name=\"logcat\">디버그 정보 내보내기</string>\n    <string name=\"version_x\">버전(%s)</string>\n    <string name=\"app_version\">버전</string>\n    <string name=\"oss_licenses\">오픈 소스 라이선스</string>\n    <string name=\"telegram\">텔레그램 업데이트 채널</string>\n    <string name=\"github\">소스 코드</string>\n    <string name=\"project\">프로젝트</string>\n    <string name=\"app_desc\">코틀린에 기록 된 안드로이드에 대한 보편적 인 프록시 도구 체인.</string>\n    <string name=\"quick_toggle\">비녀장</string>\n    <string name=\"group_default\">그룹 해제</string>\n    <string name=\"document\">문서</string>\n    <string name=\"theme\">테마</string>\n    <string name=\"menu_about\">대하여</string>\n    <string name=\"menu_group\">그룹</string>\n    <string name=\"menu_configuration\">구성</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-nb-rNO/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"apps_message\">%d programmer</string>\n    <string name=\"select_apps\">Velg programmer</string>\n    <string name=\"apps\">Programmer</string>\n    <string name=\"group_filter_groups\">Grupper</string>\n    <string name=\"ooc_warning\">Advarsel</string>\n    <string name=\"download\">Last ned</string>\n    <string name=\"install_from_fdroid\">Installer fra F-Droid</string>\n    <string name=\"install_from_play_store\">Installer fra Play-butikken</string>\n    <string name=\"action_download\">Last ned</string>\n    <string name=\"action_learn_more\">Lær mer</string>\n    <string name=\"confirm\">Bekreft</string>\n    <string name=\"connection_test_timeout\">Tidsavbrudd</string>\n    <string name=\"connection_test_clear_results\">Tøm testresultater</string>\n    <string name=\"connection_test\">Tilkoblingstest</string>\n    <string name=\"unavailable\">Utilgjengelig</string>\n    <string name=\"api_port\">API-port</string>\n    <string name=\"follow_system\">Følg systemforvalg</string>\n    <string name=\"night_mode\">Nattmodus</string>\n    <string name=\"license\">Lisens</string>\n    <string name=\"apply\">Bruk</string>\n    <string name=\"no\">Nei</string>\n    <string name=\"yes\">Ja</string>\n    <string name=\"proxy_cat\">Tjenerinnstillinger</string>\n    <string name=\"plugin_configure\">Sett opp …</string>\n    <string name=\"ignore_battery_optimizations_sum\">Fjern noen begrensninger</string>\n    <string name=\"donate\">Doner</string>\n    <string name=\"subscriptions\">Abonnementer</string>\n    <string name=\"not_connected\">Ikke tilkoblet</string>\n    <plurals name=\"removed\">\n        <item quantity=\"one\">Fjernet</item>\n        <item quantity=\"other\">%d elementer fjernet</item>\n    </plurals>\n    <string name=\"add_profile_methods_scan_qr_code\">Skann QR-kode</string>\n    <string name=\"share_qr_nfc\">QR-kode</string>\n    <string name=\"delete\">Fjern</string>\n    <string name=\"action_export_clipboard\">Eksporter til utklippstavle</string>\n    <string name=\"action_export_file\">Eksporter til fil</string>\n    <string name=\"show_system_apps\">Vis systemprogrammer</string>\n    <string name=\"scanning\">Skanner …</string>\n    <string name=\"search_apps\">Søk …</string>\n    <string name=\"off\">Av</string>\n    <string name=\"on\">På</string>\n    <string name=\"route_reset\">Tilbakestill</string>\n    <string name=\"route_profile\">Velg profil …</string>\n    <string name=\"route_proxy\">Mellomtjener</string>\n    <string name=\"only\">Kun</string>\n    <string name=\"prefer\">Foretrekk</string>\n    <string name=\"edit_config\">Rediger oppsett</string>\n    <string name=\"extra_headers\">Ekstra hoder</string>\n    <string name=\"encryption\">Kryptering</string>\n    <string name=\"tls\">Bruk TLS</string>\n    <string name=\"http_path\">HTTP-sti</string>\n    <string name=\"header_type\">Hodetype</string>\n    <string name=\"network\">Nettverk</string>\n    <string name=\"alter_id\">Alt-ID</string>\n    <string name=\"group_diff\">Forskjell (%s)</string>\n    <string name=\"group_show_diff\">Forskjell</string>\n    <string name=\"group_updated\">%s: Oppdaterte %d mellomtjenere</string>\n    <string name=\"group_no_difference\">%s: Ingen forskjell</string>\n    <string name=\"group_status_proxies_subscription\">%d mellomtjenere | Oppdatert %s</string>\n    <string name=\"group_status_proxies\">%d mellomtjenere</string>\n    <string name=\"group_status_empty_subscription\">Ikke oppdatert enda</string>\n    <string name=\"group_status_empty\">Tom gruppe</string>\n    <string name=\"group_edit_subscription\">Rediger abonnement</string>\n    <string name=\"group_edit\">Rediger gruppe</string>\n    <string name=\"group_create_subscription\">Opprett abonnement</string>\n    <string name=\"group_create\">Opprett gruppe</string>\n    <string name=\"group_name_required\">Gruppenavn påkrevd</string>\n    <string name=\"group_subscription_link\">Abonnementslenke</string>\n    <string name=\"group_name\">Gruppenavn</string>\n    <string name=\"group_default\">Ugruppert</string>\n    <string name=\"document\">Dokument</string>\n    <string name=\"theme\">Drakt</string>\n    <string name=\"menu_about\">Om</string>\n    <string name=\"menu_group\">Gruppe</string>\n    <string name=\"logcat\">Eksporter avlusingsinfo</string>\n    <string name=\"version_x\">Versjon (%s)</string>\n    <string name=\"app_version\">Versjon</string>\n    <string name=\"oss_licenses\">Friproglisenser</string>\n    <string name=\"telegram\">Telegram-oppdateringskanal</string>\n    <string name=\"github\">Kildekode</string>\n    <string name=\"project\">Prosjekt</string>\n    <string name=\"connecting\">Kobler til …</string>\n    <string name=\"undo\">Angre</string>\n    <string name=\"action_import_file\">Importer fra fil</string>\n    <string name=\"action_import\">Importer fra utklippstavle</string>\n    <string name=\"action_export\">Eksporter</string>\n    <string name=\"clear_profiles\">Tøm</string>\n    <string name=\"config_settings\">Oppsettsinnstillinger</string>\n    <string name=\"balancer_strategy\">Strategi</string>\n    <string name=\"balancer_type\">Type</string>\n    <string name=\"select_profile\">Velg profil</string>\n    <string name=\"add_profile\">Legg til profil</string>\n    <string name=\"share\">Del</string>\n    <string name=\"edit\">Rediger</string>\n    <string name=\"settings\">Innstillinger</string>\n    <string name=\"clipboard_empty\">Utklippstavlen er tom</string>\n    <string name=\"connect\">Koble til</string>\n    <string name=\"profile_empty\">Velg en profil</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"invalid_server\">Ugyldig tjenernavn</string>\n    <string name=\"service_proxy\">Mellomtjener-tjeneste</string>\n    <string name=\"service_vpn\">VPN-tjeneste</string>\n    <string name=\"http_host\">HTTP-vert</string>\n    <string name=\"uuid\">Bruker-ID</string>\n    <string name=\"port_http\">HTTP-mellomtjenerport</string>\n    <string name=\"port_proxy\">SOCKS5-mellomtjenerport</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode_proxy\">Kun mellomtjener</string>\n    <string name=\"clear_profiles_message\">Er du sikker på at du vil slette denne gruppen\\?</string>\n    <string name=\"profile_import_message\">Bekreft at du vil importere profilen %s\\?</string>\n    <string name=\"profile_import\">Importer profil</string>\n    <string name=\"subscription_import_message\">Bekrefter du at du vil importere abonnement %s\\? Hvis du kommer fra en kilde som du ikke har tillit til, kan dette føre til at din IP og denne oppførselen lekker.</string>\n    <string name=\"subscription_import\">Importer abonnement</string>\n    <string name=\"subscription_traffic\">%s Brukt / %s Gjenstår</string>\n    <string name=\"subscription_used\">%s Brukt</string>\n    <string name=\"group_filter_tags_nf\">Ingen tagger i dette abonnementet</string>\n    <string name=\"group_filter_groups_nf\">Ingen grupper i dette abonnementet</string>\n    <string name=\"group_filter_ns\">OOCv1 -abonnement er nødvendig for å bruke filtreringsfunksjonen</string>\n    <string name=\"group_filter_tags\">Etiketter</string>\n    <string name=\"subscription_update_message\">Oppdaterer %s…</string>\n    <string name=\"subscription_update\">Abonnementsoppdatering</string>\n    <string name=\"service_subscription\">Oppdateringstjeneste for abonnement</string>\n    <string name=\"ooc_missing_protocol\">Abonnementet krever støtte for protokoll %s, men det kan ikke bli funnet. Profiler som ikke støttes, blir ignorert.</string>\n    <string name=\"update_subscription_warning\">Proxy er ikke tilkoblet. Er du sikker på at du vil fortsette å oppdatere\\?</string>\n    <string name=\"ooc_subscription_token_invalid\">Ugyldig OOCv1 -token</string>\n    <string name=\"profile_requiring_plugin\">Profil %s krever at %s plugin er installert, men det ble ikke funnet.</string>\n    <string name=\"missing_plugin\">Plugin mangler</string>\n    <string name=\"subscription_user_agent\">Bruker agent</string>\n    <string name=\"update_when_connected_only_sum\">Forhindre lekkasje av IP -adresse</string>\n    <string name=\"update_when_connected_only\">Oppdater bare når den er tilkoblet</string>\n    <string name=\"auto_update_delay\">Forsinkelse automatisk oppdatering (i minutter)</string>\n    <string name=\"auto_update\">Automatisk oppdatering</string>\n    <string name=\"update_settings\">Oppdater innstillinger</string>\n    <string name=\"raw\">Rå</string>\n    <string name=\"deduplication_sum\">Fjern dupliserte konfigurasjoner ved oppdatering</string>\n    <string name=\"force_vmess_aead_sum\">Skriv om VMess -konfigurasjon med usikker autentiseringsmetode til AEAD (alterId 0)</string>\n    <string name=\"force_vmess_aead\">Tving VMess AEAD</string>\n    <string name=\"force_resolve_sum\">Løs alle domenenavn til IP -adresser når du oppdaterer. Vert og SNI blir automatisk lagt til hvis mulig</string>\n    <string name=\"force_resolve\">Tving løsning</string>\n    <string name=\"delete_group_prompt\">Er du sikker på at du vil fjerne denne gruppen\\?</string>\n    <string name=\"subscription_type\">Abonnementstype</string>\n    <string name=\"group_type\">Gruppetype</string>\n    <string name=\"subscription_settings\">Abonnementsinnstillinger</string>\n    <string name=\"subscription\">Abonnement</string>\n    <string name=\"group_settings\">Gruppeinnstillinger</string>\n    <string name=\"group_basic\">Grunnleggende</string>\n    <string name=\"ss_aead_provider\">Skjønnsokker AEAD- tilbygger</string>\n    <string name=\"trojan_provider\">Trojansk leverandør</string>\n    <string name=\"protocol_settings\">Protokollinnstillinger</string>\n    <string name=\"bypass_lan_in_core_only_sum\">Hvis Lineage\\'s Tillat hotspot -klienter å bruke VPN -er ikke fungerer, kan du prøve dette.</string>\n    <string name=\"bypass_lan_in_core_only\">Omgå LAN i Core bare</string>\n    <string name=\"append_http_proxy_sum\">HTTP -proxy brukes direkte fra (nettleser/ noen støttede apper), uten å gå gjennom den virtuelle NIC -enheten (Android 10+)</string>\n    <string name=\"append_http_proxy\">Legg til HTTP -proxy til VPN</string>\n    <string name=\"connection_test_unreachable\">Uoppnåelig</string>\n    <string name=\"connection_test_refused\">Tilkobling nektet</string>\n    <string name=\"connection_test_domain_not_found\">Domenet ble ikke funnet</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing er ikke tilgjengelig</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing utilgjengelig</string>\n    <string name=\"clear_traffic_statistics\">Slett trafikkstatistikk</string>\n    <string name=\"always_show_address_sum\">Vis alltid serveradressen på konfigurasjonskortet</string>\n    <string name=\"always_show_address\">Vis alltid adresse</string>\n    <string name=\"probe_interval\">Balanser observasjonsintervall</string>\n    <string name=\"random\">Tilfeldig</string>\n    <string name=\"list\">Liste</string>\n    <string name=\"enable_log_sum\">For feilsøkingsformål</string>\n    <string name=\"enable_log\">Aktiver logg</string>\n    <string name=\"auto\">Automatisk</string>\n    <string name=\"disable\">Deaktiver</string>\n    <string name=\"enable\">Muliggjøre</string>\n    <string name=\"lines\">%d linjer</string>\n    <string name=\"route_warn\">Sørg for at du har lest dokumentasjonen før du legger til egendefinerte regler, hvis ikke kan du ikke koble deg til Internett.</string>\n    <string name=\"restart\">Last inn proxy -tjenesten på nytt for å bruke endringer</string>\n    <string name=\"unsaved_changes_prompt\">Endringene er ikke lagret. Vil du spare\\?</string>\n    <string name=\"ss_cat\">Innstillinger for Shadowsocks</string>\n    <string name=\"plugin_auto_connect_unlock_only\">Denne pluginen fungerer kanskje ikke med Auto Connect</string>\n    <string name=\"plugin_untrusted\">Advarsel: Det ser ikke ut til at denne pluginen kommer fra en kjent klarert kilde.</string>\n    <string name=\"plugin_unknown\">Ukjent plugin %s</string>\n    <string name=\"plugin_disabled\">Funksjonshemmet</string>\n    <string name=\"plugin\">Plugg inn</string>\n    <string name=\"ignore_battery_optimizations\">Ignorer batterioptimaliseringer</string>\n    <string name=\"donate_info\">Jeg elsker penger</string>\n    <string name=\"deduplication\">Deduplisering</string>\n    <string name=\"no_proxies_found_in_clipboard\">Ingen fullmakter ble funnet i utklippstavlen</string>\n    <string name=\"no_proxies_found_in_file\">Ingen fullmakter ble funnet i filen</string>\n    <string name=\"no_proxies_found_in_subscription\">Ingen fullmakter ble funnet i abonnementet</string>\n    <string name=\"no_proxies_found\">Ingen fullmakter ble funnet i lenken</string>\n    <string name=\"error_title\">Feil</string>\n    <string name=\"cleartext_http_warning\">Klartekst HTTP -trafikk er usikker</string>\n    <string name=\"vpn_connected\">Tilkoblet, trykk for å kontrollere tilkoblingen</string>\n    <string name=\"deprecated\">Utfaset</string>\n    <string name=\"insecure\">Utrygg</string>\n    <string name=\"insecure_warn_sum\">Levert av Qv2ray Developer Community</string>\n    <string name=\"insecure_warn\">Sikkerhetsrådgivning</string>\n    <plurals name=\"added\">\n        <item quantity=\"one\">La til</item>\n        <item quantity=\"other\">%d fullmakter lagt til</item>\n    </plurals>\n    <string name=\"add_profile_methods_manual_settings\">Manuelle innstillinger</string>\n    <string name=\"delete_confirm_prompt\">Er du sikker på at du vil fjerne denne profilen\\?</string>\n    <string name=\"profile_config\">Profilkonfigurasjon</string>\n    <string name=\"file_manager_missing\">Enheten din mangler en standard filvelger for Android. Installer en, for eksempel materialfiler.</string>\n    <string name=\"action_import_err\">Kunne ikke importere.</string>\n    <string name=\"action_import_msg\">Importer vellykket!</string>\n    <string name=\"action_export_err\">Kunne ikke eksportere.</string>\n    <string name=\"action_export_msg\">Eksporter vellykket!</string>\n    <string name=\"action_scan_china_apps\">Skann apper i Kina</string>\n    <string name=\"action_from_link\">Fra abonnement</string>\n    <string name=\"action_create_group\">Tom gruppe</string>\n    <string name=\"circular_reference_sum\">Ruten kan ikke inneholde seg selv.</string>\n    <string name=\"circular_reference\">Sirkulær referanse</string>\n    <string name=\"chain_settings\">Kjedeinnstillinger</string>\n    <string name=\"balancer_settings\">Balanseinnstillinger</string>\n    <string name=\"balancer\">Balanser</string>\n    <string name=\"custom_config\">Egendefinert konfigur</string>\n    <string name=\"proxy_chain\">Mellomtjenerkjede</string>\n    <string name=\"reboot_required\">Kunne ikke starte VPN -tjenesten. Du må kanskje starte enheten på nytt.</string>\n    <string name=\"vpn_permission_denied\">Tillatelse nektet å opprette en VPN -tjeneste</string>\n    <string name=\"stopping\">Slår av…</string>\n    <string name=\"stop\">Stoppe</string>\n    <string name=\"service_failed\">Mislyktes:</string>\n    <string name=\"forward_success\">Mellomtjener startet.</string>\n    <string name=\"direct_boot_aware_summary\">Den valgte profilinformasjonen din blir mindre beskyttet</string>\n    <string name=\"direct_boot_aware\">Tillat veksling i låseskjerm</string>\n    <string name=\"auto_connect_summary\">Aktiver mellomtjener ved oppstart/programoppgradering hvis den kjørte før</string>\n    <string name=\"auto_connect\">Automatisk tilkobling</string>\n    <string name=\"bypass_apps\">Omgå</string>\n    <string name=\"clear_selections\">Fjern markeringene</string>\n    <string name=\"invert_selections\">Inverter valg</string>\n    <string name=\"proxied_apps_summary\">Konfigurer VPN -modus for utvalgte apper</string>\n    <string name=\"proxied_apps\">Apper VPN -modus</string>\n    <string name=\"tcp_keep_alive_interval\">TCP beholder det aktive pakkeleveringsintervallet</string>\n    <string name=\"mux_for_all_sum\">Hvis serveren ikke støtter Mux, kan vi ikke koble til den.</string>\n    <string name=\"mux_for_all\">Aktiver Mux for alle mulige støttede protokoller</string>\n    <string name=\"mux_concurrency\">Mux samtidige tilkoblinger</string>\n    <string name=\"mux_sum\">Mux er designet for å redusere ventetid for TCP -håndtrykk, ikke for å øke tilkoblingsmengden. Å bruke Mux til å se på videoer, nedlasting eller hastighetstest er vanligvis kontraproduktivt</string>\n    <string name=\"enable_mux\">Aktiver Multiplexer</string>\n    <string name=\"traffic_sniffing\">Aktiver trafikksniff</string>\n    <string name=\"domain_strategy\">Domeneløsningsstrategi</string>\n    <string name=\"route_opt_block_ads\">Blokker annonser</string>\n    <string name=\"route_opt_bypass_lan\">Omgå LAN</string>\n    <string name=\"route_not_asset\">Ikke en ressursfil: unntatt .dat, men %s</string>\n    <string name=\"route_asset_updated\">Oppdatert</string>\n    <string name=\"route_asset_no_update\">Ingen oppdatering</string>\n    <string name=\"route_asset_status\">Lokal versjon: %s</string>\n    <string name=\"route_rules_official\">Offisielt</string>\n    <string name=\"route_rules_provider\">Leverandør av regelaktiver</string>\n    <string name=\"route_assets\">Eiendeler</string>\n    <string name=\"route_manage_assets\">Administrer ruteelementer</string>\n    <string name=\"route_reverse_redirect\">Omvendt viderekobling</string>\n    <string name=\"route_reverse\">Omvendt fullmakt</string>\n    <string name=\"route_bypass_ip\">IP -regel for %s</string>\n    <string name=\"route_bypass_domain\">Domeneregel for %s</string>\n    <string name=\"empty_route_notice\">Sett noen regler før du lagrer</string>\n    <string name=\"empty_route\">Tom rute</string>\n    <string name=\"route_block\">Blokkere</string>\n    <string name=\"route_bypass\">Omgå</string>\n    <string name=\"route_name\">Rutenavn</string>\n    <string name=\"delete_route_prompt\">Er du sikker på at du vil fjerne denne ruten\\?</string>\n    <string name=\"route_add\">Lag rute</string>\n    <string name=\"menu_route\">Rute</string>\n    <string name=\"metered_summary\">Tips system for å behandle VPN som målt</string>\n    <string name=\"metered\">Målt hint</string>\n    <string name=\"ipv6\">IPv6 -rute</string>\n    <string name=\"config_type\">Konfig Type</string>\n    <string name=\"early_data_header_name\">Tidlig navn på dataoverskrift</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Festet sertifikatkjede</string>\n    <string name=\"certificates\">Sertifikater</string>\n    <string name=\"alpn\">Forhandling av applikasjonslagsprotokoll</string>\n    <string name=\"sni\">Servernavn indikasjon</string>\n    <string name=\"grpc_service_name\">gRPC -tjenestenavn</string>\n    <string name=\"kcp_seed\">mKCP frø</string>\n    <string name=\"quic_key\">Quic NØKKEL</string>\n    <string name=\"quic_security\">QUIC Sikkerhet</string>\n    <string name=\"ws_path\">WebSocket -bane</string>\n    <string name=\"ws_host\">WebSocket -vert</string>\n    <string name=\"security\">Kryptering av transportlag</string>\n    <string name=\"obfs_param\">Obs Stopp</string>\n    <string name=\"protocol_param\">Protokoll Param</string>\n    <string name=\"protocol\">Protokoll</string>\n    <string name=\"enc_method\">Krypter metode</string>\n    <string name=\"password\">Passord</string>\n    <string name=\"key_opt\">Nøkkel (valgfritt)</string>\n    <string name=\"password_opt\">Passord (valgfritt)</string>\n    <string name=\"username_opt\">Brukernavn (valgfritt)</string>\n    <string name=\"username\">Brukernavn</string>\n    <string name=\"server_port\">Ekstern port</string>\n    <string name=\"server_address\">Tjener</string>\n    <string name=\"profile_name\">Profil navn</string>\n    <string name=\"connection_test_url\">Tilkoblingstest -URL</string>\n    <string name=\"transproxy_mode\">Transproxy -modus</string>\n    <string name=\"require_transproxy\">Aktiver Transproxy Inbound</string>\n    <string name=\"port_local_dns\">Lokal DNS port</string>\n    <string name=\"dns_hosts\">Omskriving av domenet</string>\n    <string name=\"fakedns_message\">Kan føre til at andre applikasjoner må startes på nytt for å koble til nettverket igjen etter at proxy er stoppet</string>\n    <string name=\"enable_fakedns\">Aktiver FakeDNS</string>\n    <string name=\"dns_routing_message\">Løs domener i bypass -ruter med direkte DNS. Vær oppmerksom på potensielle DNS lekkasjer</string>\n    <string name=\"enable_dns_routing\">Aktiver DNS -ruting</string>\n    <string name=\"direct_dns\">Direkte DNS</string>\n    <string name=\"remote_dns\">Ekstern DNS</string>\n    <string name=\"cag_dns\">DNS innstillinger</string>\n    <string name=\"connection_test_error_status_code\">Feilkode: #%d</string>\n    <string name=\"connection_test_fail\">Internett utilgjengelig</string>\n    <string name=\"connection_test_error\">Mislyktes: %s</string>\n    <string name=\"connection_test_available_http\">Suksess: HTTP håndtrykk tok %dms</string>\n    <string name=\"connection_test_available\">Suksess: HTTPS håndtrykk tok %dms</string>\n    <string name=\"connection_test_testing\">Tester…</string>\n    <string name=\"speed_detail\">Proxy： %1$s↑ %2$s↓\n\\nDirekte： %3$s↑ %4$s↓</string>\n    <string name=\"allow_insecure_sum\">Deaktiver sertifikatkontroll. Når den er aktivert, er denne konfigurasjonen like sikker som ren tekst</string>\n    <string name=\"allow_insecure\">Tillat usikker</string>\n    <string name=\"security_settings\">Sikkerhetsinnstillinger</string>\n    <string name=\"show_direct_speed_sum\">Vis også trafikkhastigheten uten proxy i varselet</string>\n    <string name=\"show_direct_speed\">Vis direkte hastighet</string>\n    <string name=\"show_stop_sum\">Hvis du ikke vil bruke Quick Tile som bryter</string>\n    <string name=\"show_stop\">Vis stoppknapp</string>\n    <string name=\"cag_misc\">Diverse innstillinger</string>\n    <string name=\"speed_interval\">Oppdateringsintervall for hastighetsvarsling</string>\n    <string name=\"ws_browser_forwarding_sum\">Videresend tilsvarende WebSockets gjennom nettleseren.</string>\n    <string name=\"ws_browser_forwarding\">Videresending av nettleser</string>\n    <string name=\"cag_ws\">WebSocket -innstillinger</string>\n    <string name=\"require_http\">Aktiver HTTP inbound</string>\n    <string name=\"general_settings\">Appinnstillinger</string>\n    <string name=\"inbound_settings\">Innkommende innstillinger</string>\n    <string name=\"allow_access_sum\">Bind innkommende servere til 0.0.0.0</string>\n    <string name=\"allow_access\">Tillat tilkoblinger fra LAN</string>\n    <string name=\"cag_route\">Ruteinnstillinger</string>\n    <string name=\"port_transproxy\">Transproxy- port</string>\n    <string name=\"service_mode\">Servicemodus</string>\n    <string name=\"group_duplicate\">Duplisere:\n\\n%s</string>\n    <string name=\"group_deleted\">Slettet:\n\\n%s</string>\n    <string name=\"group_changed\">Oppdatert:\n\\n%s</string>\n    <string name=\"group_added\">La til:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">Er du sikker på at du vil fjerne denne gruppen\\?</string>\n    <string name=\"group_update\">Oppdater</string>\n    <string name=\"tile_title\">Bytter</string>\n    <string name=\"quick_toggle\">Veksle</string>\n    <string name=\"menu_configuration\">Konfigurasjon</string>\n    <string name=\"logcat_summary\">Så nyttig veldig wow</string>\n    <string name=\"app_desc\">Den universelle proxy -verktøykjeden for Android, skrevet i Kotlin.</string>\n    <string name=\"group_filter\">Raffinering</string>\n    <string name=\"standard\">Standard</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"hysteria_obfs\">Tilsløringspassord</string>\n    <string name=\"hysteria_auth_payload\">Identitetbekreftelses-nyttelast</string>\n    <string name=\"experimental_authenticated_length\">Uenderlig lengde for hver nyttelastssegment. Dette eksperimentet krever at tjener og klient bruker samme versjon av v2ray-core. Flere ødeleggende oppdateringer ved dette eksperimentet er forventet.</string>\n    <string name=\"experimental_settings\">Eksperimentelt</string>\n    <string name=\"hysteria_download_mbps\">Maks. nedlastingshastighet (i Mbps)</string>\n    <string name=\"hysteria_upload_mbps\">Maks. opplastingshastighet (i Mbps)</string>\n    <string name=\"hysteria_auth_type\">Identitetbekreftelsestype</string>\n    <string name=\"ss_stream_provider\">Shadowsocks Stream Provider</string>\n    <string name=\"menu_traffic\">Trafikk</string>\n    <string name=\"experimental_no_termination_signal\">Ikke send tilkoblingtermineringssignal som enkel dupleks for TCP-tilkobling når overført over VMess. Dette ødelegger for noen programmer.</string>\n    <string name=\"hysteria_connection_receive_window\">Mottaksvindu for QUIC-tilkobling</string>\n    <string name=\"hysteria_stream_receive_window\">Mottaksvindu for QUIC-strøm</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (Shawdowsocks-programtillegg for Android)</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Simple-obfs (Shawdowsocks-programtillegg for Android)</string>\n    <string name=\"plugin_exists_but_on_shit_system\">Profilen %s krever %s-programtillegget, men den proprietære utstyrsfabrikanten (vanligvis i overvåkningsøyemed for profitt, eller skadevare) tuklet med Android-installasjonen din, noe som gjør programtillegget ubrukelig.</string>\n    <string name=\"hysteria_disable_mtu_discovery\">Skru av oppdagelse av maksimal overførelsesenhet i sti</string>\n    <string name=\"foreground_detector_summary\">Denne tjenesten støtter rutingsregler basert på om programmer er i forgrunnen. Hvis den ikke startes er resultatet alltid i bakgrunnen.</string>\n    <string name=\"traffic_active\">Aktiv</string>\n    <string name=\"ssh_private_key_passphrase\">Passord for privat nøkkel</string>\n    <string name=\"wireguard_psk\">Forhåndsdelt nøkkel for likemann</string>\n    <string name=\"wireguard_public_key\">Offentlig nøkkel for likemann</string>\n    <string name=\"translate_platform\">Oversettelsesplattform</string>\n    <string name=\"route_need_fds\">Rutingsregelen %s avhenger av at forgrunnsoppdagelsestjenesten er operativ, men det er den ikke.</string>\n    <string name=\"route_need_vpn\">Rutingsregelen %s avhenger av at VPN er operativ, så den ignoreres.</string>\n    <string name=\"traffic_statistics_summary\">Kan øke strømforbruket</string>\n    <string name=\"group_order_origin\">Opprinnelse</string>\n    <string name=\"group_order\">Rekkefølge</string>\n    <string name=\"background\">Bakgrunn</string>\n    <string name=\"foreground\">Forgrunn</string>\n    <string name=\"foreground_status\">Forgrunnsstatus</string>\n    <string name=\"foreground_detector\">Forgrunnsoppdager</string>\n    <string name=\"traffic_holder\">Åpne VPN for å registrere trafikkstatistikk</string>\n    <string name=\"tcp_connections\">%d TCP-tilkoblinger</string>\n    <string name=\"traffic_stats\">Statistikk</string>\n    <string name=\"group_order_by_delay\">Etter forsinkelse</string>\n    <string name=\"group_order_by_name\">Etter navn</string>\n    <string name=\"wireguard_local_address\">Lokal adresse</string>\n    <string name=\"clear_logcat\">Tøm Logcat</string>\n    <string name=\"menu_log\">Logger</string>\n    <string name=\"menu_tools\">Verktøy</string>\n    <string name=\"ssh_private_key\">Privat nøkkel</string>\n    <string name=\"ssh_public_key\">Offentlig nøkkel</string>\n    <string name=\"ssh_auth_type_none\">Ingen</string>\n    <string name=\"no_statistics\">Ingen statistikk enda</string>\n    <string name=\"route_for\">Regel for %s</string>\n    <string name=\"app_no_launcher\">Programmet har ikke noe grensesnitt.</string>\n    <string name=\"copy_failed\">Kunne ikke kopiere.</string>\n    <string name=\"copy_success\">Kopiert.</string>\n    <string name=\"create_rule\">Opprett regel</string>\n    <string name=\"open_market\">Åpne markedet</string>\n    <string name=\"open_settings\">Åpne innstillinger</string>\n    <string name=\"open_app\">Åpne program</string>\n    <string name=\"copy_package_name\">Kopier pakkenavn</string>\n    <string name=\"copy_label\">Kopier navn</string>\n    <string name=\"udp_connections\">%d UDP-tilkoblinger</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"fab_color_progress\">@color/material_light_white</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"logcat\">Foutopsporing Informatie exporteren</string>\n    <string name=\"version_x\">Versie (%s)</string>\n    <string name=\"app_version\">Versie</string>\n    <string name=\"oss_licenses\">Open source-licenties</string>\n    <string name=\"telegram\">Telegram-update kanaal</string>\n    <string name=\"github\">Broncode</string>\n    <string name=\"project\">Project</string>\n    <string name=\"app_desc\">De universele proxy-toolchain voor Android, geschreven in Kotlin.</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-pt-rBR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"group_name\">Nome do grupo</string>\n    <string name=\"group_default\">Desagrupado</string>\n    <string name=\"document\">Documento</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"menu_about\">Sobre</string>\n    <string name=\"menu_group\">Grupo</string>\n    <string name=\"menu_configuration\">Configuração</string>\n    <string name=\"logcat_summary\">Tão útil muito uau</string>\n    <string name=\"logcat\">Exportar informações de depuração</string>\n    <string name=\"version_x\">Versão (%s)</string>\n    <string name=\"app_version\">Versão</string>\n    <string name=\"oss_licenses\">Licenças de código aberto</string>\n    <string name=\"telegram\">Canal de atualização do telegram</string>\n    <string name=\"github\">Código fonte</string>\n    <string name=\"project\">Projeto</string>\n    <string name=\"app_desc\">Um conjunto de ferramentas de proxy universal para Android, escrito em Kotlin.</string>\n    <string name=\"group_subscription_link\">Link de inscrição</string>\n    <string name=\"group_update\">Atualizar</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_type\">Тип заголовка</string>\n    <string name=\"network\">Сеть</string>\n    <string name=\"security\">Шифрование транспортного уровня</string>\n    <string name=\"protocol\">Протокол</string>\n    <string name=\"enc_method\">Метод шифрования</string>\n    <string name=\"password\">Пароль</string>\n    <string name=\"key_opt\">Ключ (необязательно)</string>\n    <string name=\"password_opt\">Пароль (необязательно)</string>\n    <string name=\"username\">Логин</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"transproxy_mode\">Транспрокси-режим</string>\n    <string name=\"require_transproxy\">Включить входящий прокси-сервер</string>\n    <string name=\"port_local_dns\">Локальный порт DNS</string>\n    <string name=\"dns_hosts\">Перезапись домена</string>\n    <string name=\"fakedns_message\">Может вызвать необходимость перезапуска других приложений для повторного подключения к сети после остановки прокси</string>\n    <string name=\"enable_fakedns\">Включить FakeDNS</string>\n    <string name=\"dns_routing_message\">Разрешение доменов в обходных маршрутах с помощью Direct DNS. Помните о потенциальных утечках DNS</string>\n    <string name=\"enable_dns_routing\">Включить маршрутизацию DNS</string>\n    <string name=\"direct_dns\">Прямой DNS</string>\n    <string name=\"remote_dns\">Удаленный DNS</string>\n    <string name=\"cag_dns\">Настройки DNS</string>\n    <string name=\"connection_test_error_status_code\">Код ошибки: #%d</string>\n    <string name=\"connection_test_fail\">Интернет недоступен</string>\n    <string name=\"connection_test_error\">Ошибка: %s</string>\n    <string name=\"connection_test_available_http\">Успех: рукопожатие HTTP заняло %dms</string>\n    <string name=\"connection_test_available\">Успех: рукопожатие HTTPS заняло %dms</string>\n    <string name=\"connection_test_testing\">Тестирование…</string>\n    <string name=\"speed_detail\">Прокси ：%1$s↑ %2$s↓\n\\nПрямой ：%3$s↑%4$s↓</string>\n    <string name=\"allow_insecure_sum\">Отключить проверку сертификата. При включении эта конфигурация безопасна как обычный текст</string>\n    <string name=\"allow_insecure\">Разрешить небезопасный</string>\n    <string name=\"security_settings\">Настройки безопасности</string>\n    <string name=\"show_direct_speed_sum\">Также в уведомлении показывать скорость трафика без прокси</string>\n    <string name=\"show_direct_speed\">Показать прямую скорость</string>\n    <string name=\"show_stop_sum\">Если вы не хотите использовать Quick Tile в качестве переключателя</string>\n    <string name=\"show_stop\">Показать кнопку остановки</string>\n    <string name=\"cag_misc\">Прочие настройки</string>\n    <string name=\"speed_interval\">Интервал обновления уведомлений о скорости</string>\n    <string name=\"ws_browser_forwarding_sum\">Перенаправьте соответствующие веб-сокеты через браузер.</string>\n    <string name=\"ws_browser_forwarding\">Пересылка через браузер</string>\n    <string name=\"cag_ws\">Настройки WebSocket</string>\n    <string name=\"require_http\">Включить входящий HTTP-трафик</string>\n    <string name=\"general_settings\">Настройки приложения</string>\n    <string name=\"inbound_settings\">Входящие настройки</string>\n    <string name=\"allow_access_sum\">Привязать серверы входящей почты к 0.0.0.0</string>\n    <string name=\"allow_access\">Разрешить подключения из локальной сети</string>\n    <string name=\"cag_route\">Настройки маршрута</string>\n    <string name=\"port_http\">Порт прокси HTTP</string>\n    <string name=\"service_mode_proxy\">Только прокси</string>\n    <string name=\"service_mode\">Сервисный режим</string>\n    <string name=\"group_duplicate\">Дубликат:\n\\n%s</string>\n    <string name=\"group_deleted\">Удалено:\n\\n%s</string>\n    <string name=\"group_changed\">Обновлено:\n\\n%s</string>\n    <string name=\"group_added\">Добавлен:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">Вы уверены, что хотите удалить эту группу\\?</string>\n    <string name=\"group_diff\">различия (%s)</string>\n    <string name=\"group_show_diff\">разница</string>\n    <string name=\"group_updated\">%s: обновлено %d прокси</string>\n    <string name=\"group_no_difference\">%s: Без разницы</string>\n    <string name=\"group_status_proxies_subscription\">%d Прокси | Обновлено %s</string>\n    <string name=\"group_status_proxies\">%d прокси</string>\n    <string name=\"group_status_empty_subscription\">Еще не обновлено</string>\n    <string name=\"group_status_empty\">Пустая группа</string>\n    <string name=\"group_edit_subscription\">Изменить подписку</string>\n    <string name=\"group_edit\">Редактировать группу</string>\n    <string name=\"group_create_subscription\">Создать подписку</string>\n    <string name=\"group_create\">Создать группу</string>\n    <string name=\"group_name_required\">Требуется название группы</string>\n    <string name=\"group_subscription_link\">Ссылка для подписки</string>\n    <string name=\"group_update\">Обновлять</string>\n    <string name=\"group_name\">Имя группы</string>\n    <string name=\"tile_title\">Коммутатор</string>\n    <string name=\"quick_toggle\">Переключать</string>\n    <string name=\"group_default\">Разгруппирован</string>\n    <string name=\"document\">Документ</string>\n    <string name=\"theme\">Тема</string>\n    <string name=\"menu_about\">об</string>\n    <string name=\"menu_group\">Группа</string>\n    <string name=\"menu_configuration\">Конфигурация</string>\n    <string name=\"logcat_summary\">Такой полезный очень вау</string>\n    <string name=\"logcat\">Экспорт отладочной информации</string>\n    <string name=\"version_x\">Версия (%s)</string>\n    <string name=\"server_port\">Удаленный порт</string>\n    <string name=\"server_address\">Сервер</string>\n    <string name=\"profile_name\">Имя профиля</string>\n    <string name=\"connection_test_url\">URL-адрес проверки подключения</string>\n    <string name=\"port_transproxy\">Транспрокси-порт</string>\n    <string name=\"port_proxy\">Порт прокси SOCKS5</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"app_version\">Версия</string>\n    <string name=\"oss_licenses\">Лицензии с открытым исходным кодом</string>\n    <string name=\"telegram\">Канал обновления Telegram</string>\n    <string name=\"github\">Исходный код</string>\n    <string name=\"project\">Проект</string>\n    <string name=\"app_desc\">Универсальный набор инструментов прокси для Android, написанный на Kotlin.</string>\n    <plurals name=\"added\">\n        <item quantity=\"one\">Добавлено</item>\n        <item quantity=\"few\">Добавлено %d прокси</item>\n        <item quantity=\"many\">%d прокси добавлены</item>\n        <item quantity=\"other\">%d прокси добавлены</item>\n    </plurals>\n    <plurals name=\"removed\">\n        <item quantity=\"one\">Удалено</item>\n        <item quantity=\"few\">%d элемента удалены</item>\n        <item quantity=\"many\">%d элементов удалено</item>\n        <item quantity=\"other\">%d элементов удалено</item>\n    </plurals>\n    <string name=\"mux_sum\">Mux разработан для уменьшения задержки установления связи TCP, а не для увеличения пропускной способности соединения. Использование Mux для просмотра видео, загрузки или проверки скорости обычно контрпродуктивно</string>\n    <string name=\"apps_message\">%d приложений</string>\n    <string name=\"select_apps\">Выберите приложения</string>\n    <string name=\"apps\">Приложения</string>\n    <string name=\"clear_profiles_message\">Вы уверены, что хотите очистить эту группу\\?</string>\n    <string name=\"profile_import_message\">Подтвердите, что хотите импортировать профиль %s\\?</string>\n    <string name=\"profile_import\">Импортировать профиль</string>\n    <string name=\"subscription_import_message\">Подтвердите, что хотите импортировать подписку %s\\? Если вы поступаете из ненадежного источника, это может привести к утечке вашего IP-адреса и такого поведения.</string>\n    <string name=\"subscription_import\">Импортировать подписку</string>\n    <string name=\"subscription_traffic\">%s использовано / %s осталось</string>\n    <string name=\"subscription_used\">%s Использовано</string>\n    <string name=\"group_filter_tags_nf\">В этой подписке нет тегов</string>\n    <string name=\"group_filter_groups_nf\">В этой подписке нет групп</string>\n    <string name=\"group_filter_ns\">Для использования функции фильтрации требуется подписка OOCv1</string>\n    <string name=\"group_filter_tags\">Теги</string>\n    <string name=\"group_filter_groups\">Группы</string>\n    <string name=\"group_filter\">Фильтр</string>\n    <string name=\"subscription_update_message\">Обновление %s …</string>\n    <string name=\"subscription_update\">Обновление подписки</string>\n    <string name=\"service_subscription\">Служба обновления подписки</string>\n    <string name=\"ooc_missing_protocol\">Подписка требует поддержки протокола %s, но ее нельзя найти. Неподдерживаемые профили игнорируются.</string>\n    <string name=\"ooc_warning\">Предупреждение</string>\n    <string name=\"update_subscription_warning\">Прокси-сервер не подключен, вы уверены, что хотите продолжить обновление\\?</string>\n    <string name=\"ooc_subscription_token_invalid\">Недействительный токен OOCv1</string>\n    <string name=\"download\">Скачать</string>\n    <string name=\"install_from_fdroid\">Установить с F-Droid</string>\n    <string name=\"install_from_play_store\">Установить из Play Store</string>\n    <string name=\"action_download\">СКАЧАТЬ</string>\n    <string name=\"action_learn_more\">ВЫУЧИТЬ БОЛЬШЕ</string>\n    <string name=\"profile_requiring_plugin\">Профиль %s требует установки подключаемого модуля %s, но он не найден.</string>\n    <string name=\"missing_plugin\">Отсутствующий плагин</string>\n    <string name=\"confirm\">Подтверждать</string>\n    <string name=\"subscription_user_agent\">Пользовательский агент</string>\n    <string name=\"update_when_connected_only_sum\">Предотвратить утечку IP-адреса</string>\n    <string name=\"update_when_connected_only\">Обновлять только при подключении</string>\n    <string name=\"auto_update_delay\">Задержка автоматического обновления (в минутах)</string>\n    <string name=\"auto_update\">Автоматическое обновление</string>\n    <string name=\"update_settings\">Обновить настройки</string>\n    <string name=\"force_resolve_sum\">При обновлении преобразовать все доменные имена в IP-адреса. Хост и SNI будут добавлены автоматически, если это возможно</string>\n    <string name=\"raw\">Сырой</string>\n    <string name=\"deduplication_sum\">Удалите повторяющиеся конфигурации при обновлении</string>\n    <string name=\"force_vmess_aead_sum\">Перепишите конфигурацию VMess с небезопасным методом аутентификации в AEAD (alterId 0)</string>\n    <string name=\"force_vmess_aead\">Принудительно использовать VMess AEAD</string>\n    <string name=\"force_resolve\">Сила разрешить</string>\n    <string name=\"delete_group_prompt\">Вы уверены, что хотите удалить эту группу\\?</string>\n    <string name=\"subscription_type\">Тип подписки</string>\n    <string name=\"group_type\">Тип группы</string>\n    <string name=\"subscription_settings\">Настройки подписки</string>\n    <string name=\"subscription\">Подписка</string>\n    <string name=\"group_settings\">Настройки группы</string>\n    <string name=\"group_basic\">Базовый</string>\n    <string name=\"ss_aead_provider\">Провайдер Shadowsocks AEAD</string>\n    <string name=\"trojan_provider\">Троянский провайдер</string>\n    <string name=\"protocol_settings\">Настройки протокола</string>\n    <string name=\"bypass_lan_in_core_only_sum\">Если Lineage Разрешить клиентам точки доступа использовать VPN не работает, попробуйте это.</string>\n    <string name=\"bypass_lan_in_core_only\">Обход LAN только в ядре</string>\n    <string name=\"append_http_proxy_sum\">HTTP-прокси будет использоваться напрямую из (браузера / некоторых поддерживаемых приложений), без использования виртуального сетевого адаптера (Android 10+)</string>\n    <string name=\"append_http_proxy\">Добавить HTTP-прокси к VPN</string>\n    <string name=\"connection_test_timeout\">Тайм-аут</string>\n    <string name=\"connection_test_unreachable\">Недоступен</string>\n    <string name=\"connection_test_refused\">В соединении отказано</string>\n    <string name=\"connection_test_domain_not_found\">Домен не найден</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing недоступен</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing недоступен</string>\n    <string name=\"connection_test_clear_results\">Четкие результаты теста</string>\n    <string name=\"connection_test\">Тест подключения</string>\n    <string name=\"clear_traffic_statistics\">Очистить статистику трафика</string>\n    <string name=\"always_show_address_sum\">Всегда отображать адрес сервера на карте конфигурации</string>\n    <string name=\"always_show_address\">Всегда показывать адрес</string>\n    <string name=\"unavailable\">Недоступен</string>\n    <string name=\"standard\">Стандарт</string>\n    <string name=\"probe_interval\">Интервал наблюдения балансировщика</string>\n    <string name=\"api_port\">Порт API</string>\n    <string name=\"random\">Случайный</string>\n    <string name=\"list\">Список</string>\n    <string name=\"enable_log_sum\">В целях отладки</string>\n    <string name=\"enable_log\">Включить журнал</string>\n    <string name=\"auto\">Авто</string>\n    <string name=\"disable\">Запрещать</string>\n    <string name=\"enable\">включить</string>\n    <string name=\"follow_system\">Следить за системой</string>\n    <string name=\"night_mode\">Ночной режим</string>\n    <string name=\"lines\">%d строк</string>\n    <string name=\"route_warn\">Перед добавлением собственных правил убедитесь, что вы прочитали документацию, иначе вы не сможете подключиться к Интернету.</string>\n    <string name=\"license\">Лицензия</string>\n    <string name=\"restart\">Перезагрузите прокси-сервис, чтобы применить изменения</string>\n    <string name=\"apply\">Применять</string>\n    <string name=\"no\">Нет</string>\n    <string name=\"yes\">да</string>\n    <string name=\"unsaved_changes_prompt\">Изменения не сохраняются. Вы хотите сохранить\\?</string>\n    <string name=\"ss_cat\">Настройки Shadowsocks</string>\n    <string name=\"proxy_cat\">Настройки сервера</string>\n    <string name=\"plugin_auto_connect_unlock_only\">Этот плагин может не работать с Автоматическое подключение</string>\n    <string name=\"plugin_untrusted\">Предупреждение: похоже, что этот плагин не из известного надежного источника.</string>\n    <string name=\"plugin_unknown\">Неизвестный плагин %s</string>\n    <string name=\"plugin_disabled\">инвалид</string>\n    <string name=\"plugin_configure\">Настроить…</string>\n    <string name=\"plugin\">Плагин</string>\n    <string name=\"ignore_battery_optimizations_sum\">Снять некоторые ограничения</string>\n    <string name=\"ignore_battery_optimizations\">Игнорировать оптимизацию батареи</string>\n    <string name=\"donate_info\">Я люблю деньги</string>\n    <string name=\"donate\">Пожертвовать</string>\n    <string name=\"deduplication\">Дедупликация</string>\n    <string name=\"no_proxies_found_in_clipboard\">В буфере обмена не найдено ни одного прокси</string>\n    <string name=\"no_proxies_found_in_file\">В файле прокси не найдено</string>\n    <string name=\"no_proxies_found_in_subscription\">В подписке не найдено прокси</string>\n    <string name=\"no_proxies_found\">По ссылке прокси не найдено</string>\n    <string name=\"error_title\">Ошибка</string>\n    <string name=\"cleartext_http_warning\">Открытый текст HTTP-трафика небезопасен</string>\n    <string name=\"subscriptions\">Подписки</string>\n    <string name=\"not_connected\">Не подключен</string>\n    <string name=\"vpn_connected\">Подключено, нажмите, чтобы проверить подключение</string>\n    <string name=\"connecting\">Подключение…</string>\n    <string name=\"deprecated\">Устарело</string>\n    <string name=\"insecure\">Ненадежный</string>\n    <string name=\"insecure_warn_sum\">Предоставлено сообществом разработчиков Qv2ray</string>\n    <string name=\"insecure_warn\">Консультации по безопасности</string>\n    <string name=\"undo\">Отменить</string>\n    <string name=\"add_profile_methods_manual_settings\">Ручные настройки</string>\n    <string name=\"add_profile_methods_scan_qr_code\">Отсканировать QR-код</string>\n    <string name=\"share_qr_nfc\">QR код</string>\n    <string name=\"delete_confirm_prompt\">Вы уверены, что хотите удалить этот профиль\\?</string>\n    <string name=\"delete\">Удалять</string>\n    <string name=\"profile_config\">Конфигурация профиля</string>\n    <string name=\"file_manager_missing\">На вашем устройстве отсутствует стандартный селектор файлов Android, установите его, например Material Files.</string>\n    <string name=\"action_import_err\">Не удалось импортировать.</string>\n    <string name=\"action_import_msg\">Успешный импорт!</string>\n    <string name=\"action_export_err\">Не удалось экспортировать.</string>\n    <string name=\"action_export_msg\">Успешно экспорт!</string>\n    <string name=\"action_import_file\">Импортировать из файла</string>\n    <string name=\"action_import\">Импорт из буфера обмена</string>\n    <string name=\"action_export\">Экспорт</string>\n    <string name=\"action_export_clipboard\">Экспорт в буфер обмена</string>\n    <string name=\"action_export_file\">Экспорт в файл</string>\n    <string name=\"action_scan_china_apps\">Сканировать китайские приложения</string>\n    <string name=\"action_from_link\">По подписке</string>\n    <string name=\"action_create_group\">Пустая группа</string>\n    <string name=\"clear_profiles\">Прозрачный</string>\n    <string name=\"circular_reference_sum\">Маршрут не может содержать себя.</string>\n    <string name=\"circular_reference\">Циркулярная ссылка</string>\n    <string name=\"config_settings\">Настройки конфигурации</string>\n    <string name=\"chain_settings\">Настройки цепочки</string>\n    <string name=\"balancer_strategy\">Стратегия</string>\n    <string name=\"balancer_type\">Тип</string>\n    <string name=\"balancer_settings\">Настройки балансировщика</string>\n    <string name=\"balancer\">Балансир</string>\n    <string name=\"custom_config\">Пользовательская конфигурация</string>\n    <string name=\"proxy_chain\">Прокси-цепочка</string>\n    <string name=\"select_profile\">Выбрать профиль</string>\n    <string name=\"add_profile\">Добавить профиль</string>\n    <string name=\"share\">Делиться</string>\n    <string name=\"edit\">Редактировать</string>\n    <string name=\"settings\">Настройки</string>\n    <string name=\"clipboard_empty\">Буфер обмена пуст</string>\n    <string name=\"connect\">Соединять</string>\n    <string name=\"profile_empty\">Пожалуйста, выберите профиль</string>\n    <string name=\"reboot_required\">Не удалось запустить службу VPN. Возможно, вам потребуется перезагрузить устройство.</string>\n    <string name=\"vpn_permission_denied\">Отказано в разрешении на создание службы VPN</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"stopping\">Выключение…</string>\n    <string name=\"stop\">Останавливаться</string>\n    <string name=\"service_failed\">Не удалось:</string>\n    <string name=\"invalid_server\">Неверное имя сервера</string>\n    <string name=\"forward_success\">Прокси запустился.</string>\n    <string name=\"service_proxy\">Прокси-сервис</string>\n    <string name=\"service_vpn\">VPN-сервис</string>\n    <string name=\"direct_boot_aware_summary\">Выбранная вами информация профиля будет менее защищена</string>\n    <string name=\"direct_boot_aware\">Разрешить переключение на экране блокировки</string>\n    <string name=\"auto_connect_summary\">Включить прокси при запуске/обновлении приложения, если он был запущен раньше</string>\n    <string name=\"auto_connect\">Автоматическое подключение</string>\n    <string name=\"show_system_apps\">Показать системные приложения</string>\n    <string name=\"bypass_apps\">Обход</string>\n    <string name=\"clear_selections\">Очистить выбор</string>\n    <string name=\"invert_selections\">Инвертировать выбор</string>\n    <string name=\"scanning\">Сканирование…</string>\n    <string name=\"search_apps\">Поиск…</string>\n    <string name=\"off\">Выключенный</string>\n    <string name=\"on\">На</string>\n    <string name=\"proxied_apps_summary\">Настроить режим VPN для выбранных приложений</string>\n    <string name=\"proxied_apps\">Режим VPN для приложений</string>\n    <string name=\"tcp_keep_alive_interval\">TCP сохраняет активный интервал доставки пакетов</string>\n    <string name=\"mux_for_all_sum\">Если сервер не поддерживает Mux, мы не сможем к нему подключиться.</string>\n    <string name=\"mux_for_all\">Включите Mux для всех возможных поддерживаемых протоколов</string>\n    <string name=\"mux_concurrency\">Mux одновременных подключений</string>\n    <string name=\"enable_mux\">Включить мультиплексор</string>\n    <string name=\"traffic_sniffing\">Включить анализ трафика</string>\n    <string name=\"domain_strategy\">Стратегия разрешения доменов</string>\n    <string name=\"route_opt_block_ads\">Блокировать рекламу</string>\n    <string name=\"route_opt_bypass_lan\">Обход LAN</string>\n    <string name=\"route_not_asset\">Не файл ресурса: кроме .dat, но %s</string>\n    <string name=\"route_asset_updated\">Обновлено</string>\n    <string name=\"route_asset_no_update\">Без обновления</string>\n    <string name=\"route_asset_status\">Локальная версия: %s</string>\n    <string name=\"route_rules_official\">Официальный</string>\n    <string name=\"route_rules_provider\">Поставщик активов правила</string>\n    <string name=\"route_assets\">Ресурсы</string>\n    <string name=\"route_manage_assets\">Управление активами маршрута</string>\n    <string name=\"route_reverse_redirect\">Обратное перенаправление</string>\n    <string name=\"route_reverse\">Обратный прокси</string>\n    <string name=\"route_reset\">Перезагрузить</string>\n    <string name=\"route_bypass_ip\">Правило IP для %s</string>\n    <string name=\"route_bypass_domain\">Правило домена для %s</string>\n    <string name=\"empty_route_notice\">Установите некоторые правила перед сохранением</string>\n    <string name=\"empty_route\">Пустой маршрут</string>\n    <string name=\"route_profile\">Выбрать профиль…</string>\n    <string name=\"route_block\">Блокировать</string>\n    <string name=\"route_bypass\">Обход</string>\n    <string name=\"route_proxy\">Прокси</string>\n    <string name=\"route_name\">Название маршрута</string>\n    <string name=\"delete_route_prompt\">Вы уверены, что хотите удалить этот маршрут\\?</string>\n    <string name=\"route_add\">Создать маршрут</string>\n    <string name=\"menu_route\">Маршрут</string>\n    <string name=\"metered_summary\">Система подсказок для обработки VPN как измеряемой</string>\n    <string name=\"metered\">Подсказка по дозировке</string>\n    <string name=\"only\">Только</string>\n    <string name=\"prefer\">Предпочитать</string>\n    <string name=\"ipv6\">Маршрут IPv6</string>\n    <string name=\"edit_config\">Изменить конфигурацию</string>\n    <string name=\"config_type\">Тип конфигурации</string>\n    <string name=\"extra_headers\">Дополнительные заголовки</string>\n    <string name=\"encryption\">Шифрование</string>\n    <string name=\"early_data_header_name\">Имя заголовка ранних данных</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Цепочка закрепленных сертификатов</string>\n    <string name=\"certificates\">Сертификаты</string>\n    <string name=\"alpn\">Согласование протокола уровня приложений</string>\n    <string name=\"sni\">Индикация имени сервера</string>\n    <string name=\"tls\">Использовать TLS</string>\n    <string name=\"grpc_service_name\">Имя службы gRPC</string>\n    <string name=\"kcp_seed\">mKCP Seed</string>\n    <string name=\"quic_key\">Быстрый КЛЮЧ</string>\n    <string name=\"quic_security\">QUIC Безопасность</string>\n    <string name=\"http_path\">HTTP-путь</string>\n    <string name=\"http_host\">HTTP-хост</string>\n    <string name=\"ws_path\">Путь к WebSocket</string>\n    <string name=\"ws_host\">Хост WebSocket</string>\n    <string name=\"alter_id\">Изменить ID</string>\n    <string name=\"uuid\">Логин пользователя</string>\n    <string name=\"obfs_param\">Obfs параметр</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"protocol_param\">Параметр протокола</string>\n    <string name=\"username_opt\">Имя пользователя (необязательно)</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"hysteria_download_mbps\">Максимальная скорость загрузки (в Мбит / с)</string>\n    <string name=\"hysteria_upload_mbps\">Максимальная скорость загрузки (в Мбит / с)</string>\n    <string name=\"hysteria_auth_payload\">Полезные данные аутентификации</string>\n    <string name=\"hysteria_auth_type\">Тип аутентификации</string>\n    <string name=\"hysteria_obfs\">Обфускация пароля</string>\n    <string name=\"experimental_no_termination_signal\">Не отправлять одинарный дуплексный сигнал завершения соединения для TCP-соединения при передаче через VMess. Это сломает какое-то приложение.</string>\n    <string name=\"experimental_authenticated_length\">Сделайте длину каждого сегмента полезной нагрузки больше не податливой. Этот эксперимент требует, чтобы сервер и клиент использовали одну и ту же версию v2ray-core. Ожидаются новые важные обновления по этому эксперименту.</string>\n    <string name=\"experimental_settings\">Экспериментальный</string>\n    <string name=\"route_need_fds\">Правило маршрутизации %s полагается на работу службы детектора переднего плана, но не включено.</string>\n    <string name=\"route_need_vpn\">Правило маршрутизации %s зависит от VPN, поэтому игнорируется.</string>\n    <string name=\"background\">Фон</string>\n    <string name=\"foreground\">Передний план</string>\n    <string name=\"foreground_status\">Статус переднего плана</string>\n    <string name=\"foreground_detector_summary\">Эта служба обеспечивает поддержку правил маршрутизации на основе состояния переднего плана приложений. Если он не запущен, результат всегда остается в фоновом режиме.</string>\n    <string name=\"foreground_detector\">Детектор переднего плана</string>\n    <string name=\"route_for\">Правило для %s</string>\n    <string name=\"app_no_launcher\">У приложения нет интерфейса.</string>\n    <string name=\"copy_failed\">Не удалось скопировать.</string>\n    <string name=\"copy_success\">Копировать удачно!</string>\n    <string name=\"create_rule\">Создать правило</string>\n    <string name=\"open_market\">Открытый рынок</string>\n    <string name=\"open_settings\">Открыть настройки</string>\n    <string name=\"open_app\">Открыть приложение</string>\n    <string name=\"copy_package_name\">Копировать имя пакета</string>\n    <string name=\"copy_label\">Копировать имя</string>\n    <string name=\"traffic_holder\">Откройте VPN для записи статистики трафика</string>\n    <string name=\"udp_connections\">%d UDP-соединений</string>\n    <string name=\"tcp_connections\">%d TCP-соединений</string>\n    <string name=\"traffic_stats\">Статистика</string>\n    <string name=\"traffic_active\">Активный</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (плагин Shadowsocks для Android)</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Simple Obfs (плагин Shadowsocks для Android)</string>\n    <string name=\"plugin_exists_but_on_shit_system\">Для профиля %s требуется подключаемый модуль %s, но поставщик проприетарного оборудования (обычно крупные компании-наблюдатели и производитель вредоносных программ) подделал ваш Android, сделав модуль непригодным для использования.</string>\n    <string name=\"group_order_by_delay\">С задержкой</string>\n    <string name=\"group_order_by_name\">По имени</string>\n    <string name=\"group_order_origin\">Источник</string>\n    <string name=\"group_order\">порядок</string>\n    <string name=\"hysteria_disable_mtu_discovery\">Отключить обнаружение MTU пути</string>\n    <string name=\"hysteria_connection_receive_window\">Окно получения соединения QUIC</string>\n    <string name=\"hysteria_stream_receive_window\">Окно приема потока QUIC</string>\n    <string name=\"ss_stream_provider\">Провайдер потока Shadowsocks</string>\n    <string name=\"menu_traffic\">Движение</string>\n    <string name=\"no_statistics\">Статистики пока нет</string>\n    <string name=\"traffic_statistics_summary\">Может увеличить потребление энергии</string>\n    <string name=\"clear_logcat\">Четкие отчеты об эффективности</string>\n    <string name=\"menu_log\">Логи</string>\n    <string name=\"menu_tools\">Инструменты</string>\n    <string name=\"translate_platform\">Перевести платформу</string>\n    <string name=\"ssh_private_key_passphrase\">Парольная фраза закрытого ключа</string>\n    <string name=\"ssh_private_key\">Закрытый ключ</string>\n    <string name=\"ssh_public_key\">Открытый ключ</string>\n    <string name=\"ssh_auth_type_none\">Никто</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"version_x\">Sürüm (%s)</string>\n    <string name=\"app_version\">Sürüm</string>\n    <string name=\"oss_licenses\">Açık kaynak lisansları</string>\n    <string name=\"telegram\">Telegram güncelleme kanalı</string>\n    <string name=\"github\">Kaynak kodu</string>\n    <string name=\"project\">Proje</string>\n    <string name=\"app_desc\">Android için Kotlin ile yazılmış evrensel vekil sunucu araç zinciri.</string>\n    <string name=\"enable_dns_routing\">DNS Yönlendirmesini Etkinleştir</string>\n    <string name=\"direct_dns\">Direkt DNS</string>\n    <string name=\"remote_dns\">Uzak DNS</string>\n    <string name=\"cag_dns\">DNS Ayarları</string>\n    <string name=\"connection_test_error_status_code\">Hata kodu: #%d</string>\n    <string name=\"connection_test_fail\">Internet Bağlantısı Yok</string>\n    <string name=\"connection_test_error\">Başarısız: %s</string>\n    <string name=\"connection_test_available_http\">Başarılı: HTTP el sıkışması %dms sonra gerçekleşti</string>\n    <string name=\"connection_test_available\">Başarılı: HTTPS el sıkışması %dms sonra gerçekleşti</string>\n    <string name=\"connection_test_testing\">Test ediliyor…</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"speed_detail\">Vekil Sunucu： %1$s↑ %2$s↓\n\\nDirekt： %3$s↑ %4$s↓</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"allow_insecure_sum\">Sertifika doğrulamasını devre dışı bırakır, Etkinleştirildikten sonra yapılandırma düz metin olarak korunur</string>\n    <string name=\"allow_insecure\">Güvenliksiz Bağlantıyı Etkinleştir</string>\n    <string name=\"security_settings\">Güvenlik Ayarları</string>\n    <string name=\"show_direct_speed_sum\">Vekil sunucu olmadan oluşan veri trafiği hızını da göster</string>\n    <string name=\"show_direct_speed\">Direkt Hızı Göster</string>\n    <string name=\"show_stop_sum\">Hızlı Ayarlar kısayolunu anahtar olarak kullanmak istemiyorsanız kullanın</string>\n    <string name=\"show_stop\">Durdur Butonunu Göster</string>\n    <string name=\"cag_misc\">Diğer Ayarlar</string>\n    <string name=\"speed_interval\">Hız Bildirimi Güncelleme Aralığı</string>\n    <string name=\"ws_browser_forwarding_sum\">İlgili WebSocket bağlantılarını tarayıcı üzerinden yönlendir.</string>\n    <string name=\"ws_browser_forwarding\">Tarayıcı Yönlendirmesi</string>\n    <string name=\"cag_ws\">WebSocket Ayarları</string>\n    <string name=\"require_http\">HTTP üzerinden istekleri etkinleştir</string>\n    <string name=\"general_settings\">Uygulama Ayarları</string>\n    <string name=\"inbound_settings\">Geliş Ayarları</string>\n    <string name=\"allow_access_sum\">Gelen sunucu isteklerini 0.0.0.0 üzerine bağla</string>\n    <string name=\"allow_access\">Yerel ağdan bağlantılara izin ver</string>\n    <string name=\"cag_route\">Rota Ayarları</string>\n    <string name=\"port_transproxy\">Transproxy Portu</string>\n    <string name=\"port_http\">HTTP Vekil Sunucu Portu</string>\n    <string name=\"port_proxy\">SOCKS5 Vekil Sunucu Portu</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"service_mode_proxy\">Sadece vekil sunucu</string>\n    <string name=\"service_mode\">Servis Modu</string>\n    <string name=\"group_duplicate\">%s\n\\ntekrarlı</string>\n    <string name=\"group_deleted\">%s\n\\nsilindi</string>\n    <string name=\"group_changed\">%s\n\\ngüncellendi</string>\n    <string name=\"group_added\">%s\n\\neklendi</string>\n    <string name=\"group_delete_confirm_prompt\">Bu grubu silmek istediğinize emin misiniz\\?</string>\n    <string name=\"group_diff\">Fark (%s)</string>\n    <string name=\"group_show_diff\">Fark</string>\n    <string name=\"group_updated\">%s: %d vekil sunucu güncellendi</string>\n    <string name=\"group_no_difference\">%s: Farklılık yok</string>\n    <string name=\"group_status_proxies_subscription\">%d Vekil Sunucu | %s güncellendi</string>\n    <string name=\"group_status_proxies\">%d Vekil Sunucu</string>\n    <string name=\"group_status_empty_subscription\">Henüz güncellenmedi</string>\n    <string name=\"group_status_empty\">Boş grup</string>\n    <string name=\"group_edit_subscription\">Aboneliği düzenle</string>\n    <string name=\"group_edit\">Grubu düzenle</string>\n    <string name=\"group_create_subscription\">Abonelik oluştur</string>\n    <string name=\"group_create\">Grup oluştur</string>\n    <string name=\"group_subscription_link\">Abonelik Linki</string>\n    <string name=\"group_name_required\">Grup adı gerekli</string>\n    <string name=\"group_update\">Güncelle</string>\n    <string name=\"tile_title\">Değiştirici</string>\n    <string name=\"group_name\">Grup adı</string>\n    <string name=\"quick_toggle\">Aç/Kapat</string>\n    <string name=\"group_default\">Gruplandırılmamış</string>\n    <string name=\"document\">Döküman</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"menu_about\">Hakkında</string>\n    <string name=\"menu_group\">Grup</string>\n    <string name=\"menu_configuration\">Yapılandırma</string>\n    <string name=\"logcat_summary\">Çok faydalı wow</string>\n    <string name=\"logcat\">Hata ayıklama bilgisini dışa aktar</string>\n    <string name=\"username\">Kullanıcı adı</string>\n    <string name=\"server_port\">Uzak Bağlantı Noktası</string>\n    <string name=\"server_address\">Sunucu</string>\n    <string name=\"profile_name\">Profil ismi</string>\n    <string name=\"connection_test_url\">Bağlantı Testi URL\\'si</string>\n    <string name=\"transproxy_mode\">Transproxy Modu</string>\n    <string name=\"require_transproxy\">Transproxy Gelenini Etkinleştir</string>\n    <string name=\"port_local_dns\">Yerel DNS Bağlantı Noktası</string>\n    <string name=\"dns_hosts\">Etki alanı yeniden yazma</string>\n    <string name=\"fakedns_message\">Proxy durdurulduktan sonra ağa yeniden bağlanmak için diğer uygulamaların yeniden başlatılmasına neden olabilir</string>\n    <string name=\"enable_fakedns\">Sahte DNS aktivasyonu</string>\n    <string name=\"dns_routing_message\">Doğrudan DNS ile geçiş yollarındaki etki alanlarını çözümleyin. Potansiyel DNS sızıntılarının farkında olun</string>\n    <string name=\"certificates\">Sertifikalar</string>\n    <string name=\"sni\">Görünecek Sunucu Adı</string>\n    <string name=\"tls\">TLS kullan</string>\n    <string name=\"quic_key\">Quic Anahtarı</string>\n    <string name=\"quic_security\">QUIC Güvenliği</string>\n    <string name=\"http_path\">HTTP Konumu</string>\n    <string name=\"http_host\">HTTP Sunucu</string>\n    <string name=\"ws_path\">WebSocket Konumu</string>\n    <string name=\"ws_host\">WebSocket Sunucusu</string>\n    <string name=\"header_type\">Başlık Türü</string>\n    <string name=\"network\">Ağ</string>\n    <string name=\"security\">Taşıma katmanı şifrelemesi</string>\n    <string name=\"uuid\">Kullanıcı ID</string>\n    <string name=\"obfs_param\">Şaşırtmaca Parametresi</string>\n    <string name=\"protocol_param\">Protokol Parametresi</string>\n    <string name=\"protocol\">Protokol</string>\n    <string name=\"enc_method\">Şifreleme Metodu</string>\n    <string name=\"password\">Parola</string>\n    <string name=\"key_opt\">Anahtar (İsteğe bağlı)</string>\n    <string name=\"password_opt\">Parola (İsteğe bağlı)</string>\n    <string name=\"username_opt\">Kullanıcı adı (İsteğe bağlı)</string>\n    <string name=\"menu_traffic\">Trafik</string>\n    <string name=\"route_reverse_redirect\">Ters Yönlendirme</string>\n    <string name=\"route_reverse\">Ters Vekil Sunucu</string>\n    <string name=\"route_reset\">Sıfırla</string>\n    <string name=\"route_bypass_ip\">%s için IP kuralı</string>\n    <string name=\"route_bypass_domain\">%s için alan adı kuralı</string>\n    <string name=\"empty_route_notice\">Kaydetmeden önce bazı kurallar ayarlayın</string>\n    <string name=\"empty_route\">Boş Rota</string>\n    <string name=\"route_profile\">Profil Seç…</string>\n    <string name=\"route_block\">Engelle</string>\n    <string name=\"route_proxy\">Vekil Sunucu</string>\n    <string name=\"route_name\">Rota Adı</string>\n    <string name=\"delete_route_prompt\">Bu rotayı kaldırmak istediğinize emin misiniz\\?</string>\n    <string name=\"route_add\">Rota Oluştur</string>\n    <string name=\"metered\">Kota Belirteci</string>\n    <string name=\"metered_summary\">Sisteme VPN\\'i kotalı olarak belirt</string>\n    <string name=\"menu_route\">Rota</string>\n    <string name=\"only\">Sadece</string>\n    <string name=\"prefer\">Öner</string>\n    <string name=\"ipv6\">IPv6 Rotası</string>\n    <string name=\"edit_config\">Yapılandırmayı Düzenle</string>\n    <string name=\"config_type\">Yapılandırma Türü</string>\n    <string name=\"extra_headers\">Ekstra Başlıklar</string>\n    <string name=\"encryption\">Şifreleme</string>\n    <string name=\"early_data_header_name\">Erken Veri Başlığı Adı</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Sabitlenmiş Sertifika Zinciri</string>\n    <string name=\"bypass_apps\">Atlat</string>\n    <string name=\"route_opt_bypass_lan\">Yerel Ağı Atlat</string>\n    <string name=\"route_bypass\">Atlat</string>\n    <string name=\"route_need_fds\">%s yönlendirme kuralı Önplan Algılayıcı Servisine bağlıdır ama servis etkinleştirilmemiş.</string>\n    <string name=\"route_need_vpn\">%s yönlendirme kuralı VPN\\'in etkin olmasına bağlıdır ve bu nedenle görmezden gelindi.</string>\n    <string name=\"background\">Arkaplan</string>\n    <string name=\"foreground\">Önplan</string>\n    <string name=\"foreground_status\">Önplan Durumu</string>\n    <string name=\"foreground_detector_summary\">Bu servis uygulamaların önplan durumuna göre rota kuralları desteği sağlar. Eğer başlatılmadıysa sonuç her zaman arkaplandadır.</string>\n    <string name=\"foreground_detector\">Önplan algılayıcısı</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (Shadowsocks Android Eklentisi)</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Basit Şaşırtmaca (Shadowsocks Android Eklentisi)</string>\n    <string name=\"hysteria_connection_receive_window\">QUIC Bağlantı Alım Penceresi</string>\n    <string name=\"hysteria_stream_receive_window\">QUIC Akış Alım Penceresi</string>\n    <string name=\"experimental_no_termination_signal\">Duplex sonlandırma sinyalini VMess kullanırken TCP bağlantısı için yollama. Bu bazı uygulamaları bozacaktır.</string>\n    <string name=\"force_resolve_sum\">Güncelleme yaparken bütün alan adlarının IP adreslerini çöz. Mümkünse sunucu ve SNI otomatik olarak eklenir</string>\n    <string name=\"ss_stream_provider\">Shadowsocks Akış Sağlayıcı</string>\n    <string name=\"bypass_lan_in_core_only_sum\">Eğer Lineage\\'in Hotspot istemcilerinin VPN kullanmasına izin ver özelliği çalışmıyorsa bunu deneyin.</string>\n    <string name=\"bypass_lan_in_core_only\">Yerel Ağı Sadece Çekirdekte Atlat</string>\n    <string name=\"always_show_address_sum\">Yapılandırma kartının üzerinde her zaman sunucu adresini göster</string>\n    <string name=\"probe_interval\">Dengeleyici gözlem aralığı</string>\n    <string name=\"insecure_warn_sum\">Qv2ray Geliştirici Topluluğu Tarafından Sağlanmıştır</string>\n    <string name=\"insecure_warn\">Güvenlik Danışmanlığı</string>\n    <string name=\"add_profile_methods_manual_settings\">Elle Ayar Ekle</string>\n    <string name=\"profile_config\">Profil yapılandırması</string>\n    <string name=\"file_manager_missing\">Cihazınızda dosya yöneticisi bulunamadı, lütfen bir tane kurun, örneğin Material Files.</string>\n    <string name=\"action_from_link\">Abonelikten</string>\n    <string name=\"circular_reference_sum\">Rota kendisini içinde barındıramaz.</string>\n    <string name=\"circular_reference\">Dairesel referans</string>\n    <string name=\"config_settings\">Yapılandırma Ayarları</string>\n    <string name=\"chain_settings\">Zincir Ayarları</string>\n    <string name=\"proxy_chain\">Vekil Sunucu Zinciri</string>\n    <string name=\"plugin_configure\">Yapılandır…</string>\n    <string name=\"custom_config\">Kişisel Yapılandırma</string>\n    <string name=\"route_for\">%s için kural</string>\n    <string name=\"app_no_launcher\">Bu uygulama herhangi bir arayüze sahip değil.</string>\n    <string name=\"copy_failed\">Kopyalama başarısız oldu.</string>\n    <string name=\"copy_success\">Kopyalama başarılı!</string>\n    <string name=\"create_rule\">Kural Oluştur</string>\n    <string name=\"open_market\">Mağazayı Aç</string>\n    <string name=\"open_settings\">Ayarları Aç</string>\n    <string name=\"open_app\">Uygulamayı Aç</string>\n    <string name=\"copy_package_name\">Paket Adını Kopyala</string>\n    <string name=\"copy_label\">İsmi Kopyala</string>\n    <string name=\"traffic_holder\">Trafik istatistiklerini kaydetmek için VPN\\'i açın</string>\n    <string name=\"udp_connections\">%d UDP bağlantı</string>\n    <string name=\"tcp_connections\">%d TCP bağlantı</string>\n    <string name=\"traffic_stats\">İstatistikler</string>\n    <string name=\"traffic_active\">Etkin</string>\n    <string name=\"plugin_exists_but_on_shit_system\">Profil %s %s eklentisine ihtiyaç duyuyor, ancak kapalı kaynak ekipman sağlayıcınız (genellikle sermaye devleri ve kötü amaçlı yazılım geliştiricileri) Android ile oynamış ve eklenti bu yüzden kullanılamıyor.</string>\n    <string name=\"group_order_by_delay\">Gecikme</string>\n    <string name=\"group_order_by_name\">İsim</string>\n    <string name=\"group_order_origin\">Köken</string>\n    <string name=\"group_order\">Sıralama</string>\n    <string name=\"experimental_settings\">Deneysel</string>\n    <string name=\"hysteria_download_mbps\">En Fazla İndirme Hızı (Mbps cinsinden)</string>\n    <string name=\"hysteria_upload_mbps\">En Fazla Yükleme Hızı (Mbps cinsinden)</string>\n    <string name=\"hysteria_auth_type\">Doğrulama Türü</string>\n    <string name=\"apps_message\">%d Uygulama</string>\n    <string name=\"select_apps\">Uygulama Seç</string>\n    <string name=\"apps\">Uygulamalar</string>\n    <string name=\"clear_profiles_message\">Bu grubu temizlemek istediğinize emin misiniz\\?</string>\n    <string name=\"profile_import_message\">%s profilini gerçekten içe aktarmak istiyor musunuz\\?</string>\n    <string name=\"profile_import\">Profili içe aktar</string>\n    <string name=\"subscription_import_message\">%s aboneliğini içe aktarmayı gerçekten istiyor musunuz\\? Eğer güvenilir olmayan bir kaynaktan geliyorsanız bu IP adresinizin ve davranışınızın sızdırılmasına sebep olabilir.</string>\n    <string name=\"subscription_import\">Aboneliği içe aktar</string>\n    <string name=\"subscription_traffic\">%s Kullanıldı / %s Kaldı</string>\n    <string name=\"subscription_used\">%s Kullanıldı</string>\n    <string name=\"group_filter_tags_nf\">Bu abonelikte hiç etiket yok</string>\n    <string name=\"group_filter_groups_nf\">Bu abonelikte hiç grup yok</string>\n    <string name=\"group_filter_ns\">Filtreleme özelliği için OOCv1 aboneliği gereklidir</string>\n    <string name=\"group_filter_tags\">Etiketler</string>\n    <string name=\"group_filter_groups\">Gruplar</string>\n    <string name=\"group_filter\">Filtre</string>\n    <string name=\"subscription_update_message\">%s güncelleniyor …</string>\n    <string name=\"subscription_update\">Abonelik Güncellemesi</string>\n    <string name=\"service_subscription\">Abonelik Güncelleme Servisi</string>\n    <string name=\"ooc_missing_protocol\">Bu abonelik %s protokolü için destek gerektirir ama bulunamıyor. Desteklenmeyen profiller görmezden gelinir.</string>\n    <string name=\"ooc_warning\">Uyarı</string>\n    <string name=\"update_subscription_warning\">Vekil sunucu bağlanmadı, güncellemeye devam etmek istediğinize emin misiniz\\?</string>\n    <string name=\"ooc_subscription_token_invalid\">OOCv1 Anahtarı Geçersiz</string>\n    <string name=\"download\">İndir</string>\n    <string name=\"install_from_fdroid\">F-Droid\\'den Kur</string>\n    <string name=\"install_from_play_store\">Play Store\\'dan Kur</string>\n    <string name=\"action_download\">İNDİR</string>\n    <string name=\"action_learn_more\">DAHA FAZLASINI ÖĞREN</string>\n    <string name=\"profile_requiring_plugin\">Profil %s %s eklentisinin kurulmasını gerektirir ama kurulu değil.</string>\n    <string name=\"missing_plugin\">Eksik Eklenti</string>\n    <string name=\"confirm\">Doğrula</string>\n    <string name=\"subscription_user_agent\">Kullanıcı Kimliği</string>\n    <string name=\"update_when_connected_only_sum\">IP adresinin sızdırılmasını önle</string>\n    <string name=\"update_when_connected_only\">Sadece bağlanıldığında güncelle</string>\n    <string name=\"auto_update_delay\">Otomatik Güncelleme Gecikmesi (Dakika cinsinden)</string>\n    <string name=\"auto_update\">Otomatik Güncelle</string>\n    <string name=\"update_settings\">Güncelleme Ayarları</string>\n    <string name=\"raw\">Ham</string>\n    <string name=\"deduplication_sum\">Güncellerken tekrarlanan yapılandırmaları kaldır</string>\n    <string name=\"force_vmess_aead\">VMess AEAD\\'i Zorla</string>\n    <string name=\"force_resolve\">Çözmeye Zorla</string>\n    <string name=\"delete_group_prompt\">Bu grubu kaldırmak istediğinize emin misiniz\\?</string>\n    <string name=\"subscription_type\">Abonelik Türü</string>\n    <string name=\"group_type\">Grup Türü</string>\n    <string name=\"subscription_settings\">Abonelik Ayarları</string>\n    <string name=\"subscription\">Abonelik</string>\n    <string name=\"group_settings\">Grup Ayarları</string>\n    <string name=\"group_basic\">Basit</string>\n    <string name=\"ss_aead_provider\">Shadowsocks AEAD Sağlayıcısı</string>\n    <string name=\"trojan_provider\">Trojan Sağlayıcısı</string>\n    <string name=\"protocol_settings\">Protokol Ayarları</string>\n    <string name=\"append_http_proxy_sum\">HTTP vekil sunucu direkt tarayıcı veya bazı destekli uygulamalar tarafından, sanal NIC aygıtından geçmeden kullanılır (Android 10+)</string>\n    <string name=\"append_http_proxy\">HTTP Vekil Sunucuyu VPN\\'e Dahil Et</string>\n    <string name=\"connection_test_timeout\">Zaman Aşımı</string>\n    <string name=\"connection_test_unreachable\">Erişilemez</string>\n    <string name=\"connection_test_refused\">Bağlantı reddedildi</string>\n    <string name=\"connection_test_domain_not_found\">Alan adı bulunamadı</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing kullanılamıyor</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing kullanılamıyor</string>\n    <string name=\"connection_test_clear_results\">Test sonuçlarını temizle</string>\n    <string name=\"connection_test\">Bağlantı testi</string>\n    <string name=\"clear_traffic_statistics\">Trafik istatistiklerini temizle</string>\n    <string name=\"always_show_address\">Her Zaman Adresi Göster</string>\n    <string name=\"unavailable\">Kullanılamıyor</string>\n    <string name=\"standard\">Standart</string>\n    <string name=\"api_port\">API Portu</string>\n    <string name=\"list\">Liste</string>\n    <string name=\"random\">Rastgele</string>\n    <string name=\"enable_log_sum\">Hata ayıklama amaçlıdır</string>\n    <string name=\"enable_log\">Kayıtları Etkinleştir</string>\n    <string name=\"auto\">Otomatik</string>\n    <string name=\"disable\">Devre Dışı Bırak</string>\n    <string name=\"enable\">Etkinleştir</string>\n    <string name=\"follow_system\">Sistemi Takip Et</string>\n    <string name=\"night_mode\">Gece Modu</string>\n    <string name=\"lines\">%d Satır</string>\n    <string name=\"route_warn\">Kişisel kurallar eklemeden önce dökümanı okuduğunuza emin olun, yoksa Internet\\'e bağlanamayabilirsiniz.</string>\n    <string name=\"license\">Lisans</string>\n    <string name=\"restart\">Değişiklikleri uygulamak için vekil sunucu servisini yeniden başlatın</string>\n    <string name=\"apply\">Uygula</string>\n    <string name=\"no\">Hayır</string>\n    <string name=\"yes\">Evet</string>\n    <string name=\"unsaved_changes_prompt\">Değişiklikler kaydedilmedi. Kaydetmek ister misiniz\\?</string>\n    <string name=\"ss_cat\">Shadowsocks Ayarları</string>\n    <string name=\"proxy_cat\">Sunucu Ayarları</string>\n    <string name=\"plugin_auto_connect_unlock_only\">Bu eklenti Otomatik Bağlan ile çalışmayabilir</string>\n    <string name=\"plugin_untrusted\">Uyarı: Bu eklenti bilinen güvenilir bir kaynaktan geliyora benzemiyor.</string>\n    <string name=\"plugin_unknown\">Bilinmeyen eklenti %s</string>\n    <string name=\"plugin_disabled\">Devre Dışı</string>\n    <string name=\"plugin\">Eklenti</string>\n    <string name=\"ignore_battery_optimizations_sum\">Bazı kısıtlamaları kaldır</string>\n    <string name=\"ignore_battery_optimizations\">Pil optimizasyonlarını devre dışı bırak</string>\n    <string name=\"donate_info\">Paraya aşığım</string>\n    <string name=\"donate\">Bağış yap</string>\n    <string name=\"deduplication\">Tekilleştirme</string>\n    <string name=\"no_proxies_found_in_clipboard\">Kopyalama panosunda vekil sunucu bulunamadı</string>\n    <string name=\"no_proxies_found_in_file\">Dosya içinde vekil sunucu bulunamadı</string>\n    <string name=\"no_proxies_found_in_subscription\">Abonelik içinde vekil sunucu bulunamadı</string>\n    <string name=\"no_proxies_found\">Link içinde vekil sunucu bulunamadı</string>\n    <string name=\"error_title\">Hata</string>\n    <string name=\"cleartext_http_warning\">Düz metin HTTP trafiği güvenli değil</string>\n    <string name=\"subscriptions\">Abonelikler</string>\n    <string name=\"not_connected\">Bağlanmadı</string>\n    <string name=\"vpn_connected\">Bağlandı, bağlantıyı test etmek için dokunun</string>\n    <string name=\"connecting\">Bağlanıyor…</string>\n    <string name=\"insecure\">Güvenli Değil</string>\n    <string name=\"deprecated\">Geliştirmesi Durdurulmuş</string>\n    <string name=\"undo\">Geri Al</string>\n    <plurals name=\"added\">\n        <item quantity=\"one\">Eklendi</item>\n        <item quantity=\"other\">%d vekil sunucu eklendi</item>\n    </plurals>\n    <plurals name=\"removed\">\n        <item quantity=\"one\">Kaldırıldı</item>\n        <item quantity=\"other\">%d öğe kaldırıldı</item>\n    </plurals>\n    <string name=\"add_profile_methods_scan_qr_code\">QR Kodu Tara</string>\n    <string name=\"share_qr_nfc\">QR kodu</string>\n    <string name=\"delete_confirm_prompt\">Bu profili kaldırmak istediğinize emin misiniz\\?</string>\n    <string name=\"delete\">Kaldır</string>\n    <string name=\"action_import_err\">İçe aktarma başarısız oldu.</string>\n    <string name=\"action_import_msg\">Başarıyla içe aktarıldı!</string>\n    <string name=\"action_export_err\">Dışa aktarma başarısız oldu.</string>\n    <string name=\"action_export_msg\">Başarıyla dışa aktarıldı!</string>\n    <string name=\"action_import_file\">Dosyadan aktar</string>\n    <string name=\"action_import\">Kopyalama Panosundan Aktar</string>\n    <string name=\"action_export\">Dışa Aktar</string>\n    <string name=\"action_export_clipboard\">Kopyalama Panosuna Aktar</string>\n    <string name=\"action_export_file\">Dosyaya aktar</string>\n    <string name=\"action_scan_china_apps\">Çin Uygulamalarını Tara</string>\n    <string name=\"action_create_group\">Boş grup</string>\n    <string name=\"clear_profiles\">Temizle</string>\n    <string name=\"balancer_strategy\">Strateji</string>\n    <string name=\"balancer_type\">Tür</string>\n    <string name=\"balancer_settings\">Dengeleyici Ayarları</string>\n    <string name=\"balancer\">Dengeleyici</string>\n    <string name=\"select_profile\">Profil Seç</string>\n    <string name=\"add_profile\">Profil Ekle</string>\n    <string name=\"share\">Paylaş</string>\n    <string name=\"edit\">Düzenle</string>\n    <string name=\"settings\">Ayarlar</string>\n    <string name=\"clipboard_empty\">Kopyalama panosu boş</string>\n    <string name=\"connect\">Bağlan</string>\n    <string name=\"profile_empty\">Lütfen bir profil seçin</string>\n    <string name=\"reboot_required\">VPN servisi oluşturma başarısız oldu. Cihazınızı yeniden başlatmanız gerekiyor olabilir.</string>\n    <string name=\"vpn_permission_denied\">VPN servisi oluşturabilmek için gereken izin isteği reddedildi</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"stopping\">Kapatılıyor…</string>\n    <string name=\"stop\">Durdur</string>\n    <string name=\"service_failed\">Başarısız:</string>\n    <string name=\"invalid_server\">Geçersiz sunucu adı</string>\n    <string name=\"forward_success\">Vekil sunucu başlatıldı.</string>\n    <string name=\"service_proxy\">Vekil Sunucu Servisi</string>\n    <string name=\"service_vpn\">VPN Servisi</string>\n    <string name=\"direct_boot_aware_summary\">Seçili profil bilginiz daha az korunacak</string>\n    <string name=\"direct_boot_aware\">Kilit Ekranında Açıp Kapatmaya İzin Ver</string>\n    <string name=\"auto_connect_summary\">Eğer önceden çalışıyorsa açılışta/uygulama güncellemesinde tekrar vekil sunucuyu etkinleştir</string>\n    <string name=\"auto_connect\">Otomatik Bağlan</string>\n    <string name=\"show_system_apps\">Sistem uygulamalarını göster</string>\n    <string name=\"clear_selections\">Seçimleri temizle</string>\n    <string name=\"invert_selections\">Seçimleri ters çevir</string>\n    <string name=\"scanning\">Taranıyor…</string>\n    <string name=\"search_apps\">Ara…</string>\n    <string name=\"off\">Kapalı</string>\n    <string name=\"on\">Açık</string>\n    <string name=\"proxied_apps_summary\">VPN modunu seçili uygulamalar için ayarla</string>\n    <string name=\"proxied_apps\">Uygulama VPN modu</string>\n    <string name=\"mux_for_all_sum\">Eğer sunucu çoklayıcıyı desteklemiyorsa o sunucuya bağlanamayız.</string>\n    <string name=\"mux_for_all\">Çoklayıcıyı tüm olası desteklenen protokoller için etkinleştir</string>\n    <string name=\"mux_concurrency\">Çoklayıcı Eşzamanlı Bağlantı Sayısı</string>\n    <string name=\"mux_sum\">Çoklayıcı bağlantı verimini arttırmak için değil TCP el sıkışma gecikmesini azaltmak için tasarlanmıştır. Video izlemek, bir şeyler indirmek veya hız testleri için genellikle verimsizdir</string>\n    <string name=\"enable_mux\">Çoklayıcıyı Etkinleştir</string>\n    <string name=\"traffic_sniffing\">Trafik İzlemeyi Etkinleştir</string>\n    <string name=\"domain_strategy\">Alan Adı Çözümleme Stratejisi</string>\n    <string name=\"route_opt_block_ads\">Reklamları engelle</string>\n    <string name=\"route_asset_updated\">Güncellendi</string>\n    <string name=\"route_asset_no_update\">Güncelleme yok</string>\n    <string name=\"route_asset_status\">Yerel sürüm: %s</string>\n    <string name=\"route_rules_official\">Resmi</string>\n    <string name=\"alter_id\">Alternatif ID</string>\n    <string name=\"obfs\">Şaşırtmaca</string>\n    <string name=\"tcp_keep_alive_interval\">TCP aktif paket teslim koruma aralığı</string>\n    <string name=\"route_not_asset\">Varlık dosyası değil: .dat beklendi ama %s</string>\n    <string name=\"route_rules_provider\">Rota Varlıkları Sağlayıcısı</string>\n    <string name=\"route_assets\">Varlıklar</string>\n    <string name=\"route_manage_assets\">Rota Varlıklarını Yönet</string>\n    <string name=\"alpn\">Uygulama Katmanı Protokol Aşımı</string>\n    <string name=\"grpc_service_name\">gRPC ServisAdı</string>\n    <string name=\"kcp_seed\">mKCP Tohumu</string>\n    <string name=\"hysteria_disable_mtu_discovery\">Yol MTU Keşfini Devre Dışı Bırak</string>\n    <string name=\"experimental_authenticated_length\">Her yük bölümünun uzunluğunu artık biçimlendirilebilir yapma. Bu deneysel özellik istemci ve sunucunun aynı v2ray-core sürümünü kullanmasını gerektirir. Bu deneysel özellik için daha fazla bozucu güncelleme bekleniyor.</string>\n    <string name=\"hysteria_auth_payload\">Doğrulama Yükü</string>\n    <string name=\"hysteria_obfs\">Şaşırtmaca Parolası</string>\n    <string name=\"force_vmess_aead_sum\">VMess yapılandırmasını güvenliksiz doğrulama metoduyla AEAD\\'ye yeniden yaz (alterId 0)</string>\n    <string name=\"no_statistics\">Henüz istatistik yok</string>\n    <string name=\"traffic_statistics_summary\">Güç tüketimini arttırabilir</string>\n    <string name=\"clear_logcat\">Günlüğü Temizle</string>\n    <string name=\"menu_log\">Günlükler</string>\n    <string name=\"menu_tools\">Araçlar</string>\n    <string name=\"translate_platform\">Çeviri Platformu</string>\n    <string name=\"ssh_private_key_passphrase\">Gizli Anahtar Parolası</string>\n    <string name=\"ssh_private_key\">Gizli Anahtar</string>\n    <string name=\"ssh_public_key\">Umumi Anahtar</string>\n    <string name=\"ssh_auth_type_none\">Hiçbiri</string>\n    <string name=\"wireguard_psk\">Eş Ön Paylaşımlı Anahtarı</string>\n    <string name=\"wireguard_public_key\">Eş Umumi Anahtarı</string>\n    <string name=\"wireguard_local_address\">Yerel Adres</string>\n    <string name=\"resolve_destination_summary\">Eğer hedef adres bir alan adıysa, IPv6 stratejisine göre iletilir.</string>\n    <string name=\"resolve_destination\">Hedefi Çözümle</string>\n    <string name=\"destination_override_summary\">Sadece yönlendirme için değil, hedef adresi geçersiz kılmak için de yakalanmış alan adını kullan</string>\n    <string name=\"destination_override\">Hedefi Geçersiz Kıl</string>\n    <string name=\"tun_implementation\">TUN Tanımlaması</string>\n    <string name=\"generating\">Oluşturuluyor…</string>\n    <string name=\"warp_generate\">Yapılandırma Oluştur</string>\n    <string name=\"warp_license\">CloudFlare Warp bir ücretsiz WireGuard VPN sağlayıcısıdır. Bunu kullanarak kullanım koşullarını kabul etmiş sayılırsınız: https://www.cloudflare.com/application/terms/</string>\n    <string name=\"cloudflare_wrap\">Warp</string>\n    <string name=\"profile_traffic_statistics_summary\">Devre dışı bırakıldığında kullanılan trafik sayılmayacak</string>\n    <string name=\"pcap_summary\">Trafiği .pcap dosyasına kaydet</string>\n    <string name=\"pcap_notice\">Pcap dosyaları %s içine kaydedilecek</string>\n    <string name=\"group_filter_owners\">Sahipler</string>\n    <string name=\"group_filter_owners_nf\">Bu abonelikte hiç sahip yok</string>\n    <string name=\"app_traffic_statistics\">Uygulama Trafiği İstatistikleri</string>\n    <string name=\"profile_traffic_statistics\">Profil Trafiği İstatistikleri</string>\n    <string name=\"app_statistics_disabled\">Uygulama Trafiği istatistikleri devre dışı</string>\n    <string name=\"enable_pcap\">Pcap\\'i Etkinleştir</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"port_local_dns\">Локальний порт DNS</string>\n    <string name=\"dns_hosts\">Переписування домену</string>\n    <string name=\"fakedns_message\">Може призвести до перезапуску інших програм для повторного підключення до мережі після зупинки проксі</string>\n    <string name=\"enable_fakedns\">Увімкніть FakeDNS</string>\n    <string name=\"dns_routing_message\">Вирішуйте домени в обхідних маршрутах за допомогою Direct DNS. Будьте в курсі можливих витоків DNS</string>\n    <string name=\"enable_dns_routing\">Увімкнути маршрутизацію DNS</string>\n    <string name=\"direct_dns\">Прямий DNS</string>\n    <string name=\"remote_dns\">Віддалений DNS</string>\n    <string name=\"cag_dns\">Налаштування DNS</string>\n    <string name=\"connection_test_error_status_code\">Код помилки: #%d</string>\n    <string name=\"connection_test_fail\">Інтернет недоступний</string>\n    <string name=\"connection_test_error\">Не вдалося: %s</string>\n    <string name=\"connection_test_available_http\">Успішно: рукостискання HTTP зайняло %dms</string>\n    <string name=\"connection_test_available\">Успіх: рукостискання HTTPS зайняло %dms</string>\n    <string name=\"connection_test_testing\">Тестування…</string>\n    <string name=\"speed_detail\">Проксі: %1$s↑ %2$s↓\n\\nПрямий: %3$s↑ %4$s↓</string>\n    <string name=\"allow_insecure_sum\">Вимкнути перевірку сертифікатів. Якщо ввімкнено, ця конфігурація є такою ж безпечною, як і відкритий текст</string>\n    <string name=\"allow_insecure\">Дозволити незахищене</string>\n    <string name=\"security_settings\">Налаштування безпеки</string>\n    <string name=\"show_direct_speed_sum\">У сповіщенні також покажіть швидкість трафіку без проксі -сервера</string>\n    <string name=\"show_direct_speed\">Показати пряму швидкість</string>\n    <string name=\"show_stop_sum\">Якщо ви не хочете використовувати Quick Tile як перемикач</string>\n    <string name=\"show_stop\">Показати кнопку зупинки</string>\n    <string name=\"cag_misc\">Різні налаштування</string>\n    <string name=\"speed_interval\">Інтервал оновлення сповіщення про швидкість</string>\n    <string name=\"ws_browser_forwarding_sum\">Пересилайте відповідні WebSockets через браузер.</string>\n    <string name=\"ws_browser_forwarding\">Переадресація браузера</string>\n    <string name=\"cag_ws\">Налаштування WebSocket</string>\n    <string name=\"require_http\">Увімкнути вхідний протокол HTTP</string>\n    <string name=\"general_settings\">Налаштування програми</string>\n    <string name=\"inbound_settings\">Вхідні налаштування</string>\n    <string name=\"allow_access_sum\">Прив’язати вхідні сервери до 0.0.0.0</string>\n    <string name=\"allow_access\">Дозволити підключення з локальної мережі</string>\n    <string name=\"cag_route\">Налаштування маршруту</string>\n    <string name=\"port_transproxy\">Транспроксі -порт</string>\n    <string name=\"port_http\">Проксі -порт HTTP</string>\n    <string name=\"port_proxy\">Порт проксі -сервера SOCKS5</string>\n    <string name=\"service_mode_proxy\">Тільки проксі</string>\n    <string name=\"service_mode\">Режим обслуговування</string>\n    <string name=\"group_duplicate\">Дублікат:\n\\n%s</string>\n    <string name=\"group_deleted\">Видалено:\n\\n%s</string>\n    <string name=\"group_changed\">Оновлено:\n\\n%s</string>\n    <string name=\"group_added\">Додано:\n\\n%s</string>\n    <string name=\"group_delete_confirm_prompt\">Ви впевнені, що хочете видалити цю групу\\?</string>\n    <string name=\"group_diff\">Різниця (%s)</string>\n    <string name=\"group_show_diff\">Різниця</string>\n    <string name=\"group_updated\">%s: Оновлено %d проксі</string>\n    <string name=\"group_no_difference\">%s: Різниці немає</string>\n    <string name=\"group_status_proxies_subscription\">%d проксі | Оновлено на %s</string>\n    <string name=\"group_status_proxies\">%d проксі</string>\n    <string name=\"group_status_empty_subscription\">Ще не оновлено</string>\n    <string name=\"group_status_empty\">Порожня група</string>\n    <string name=\"group_edit_subscription\">Редагувати підписку</string>\n    <string name=\"group_edit\">Редагувати групу</string>\n    <string name=\"group_create_subscription\">Створити підписку</string>\n    <string name=\"group_create\">Створити групу</string>\n    <string name=\"group_name_required\">Потрібна назва групи</string>\n    <string name=\"group_subscription_link\">Посилання на підписку</string>\n    <string name=\"group_update\">Оновлення</string>\n    <string name=\"group_name\">Назва групи</string>\n    <string name=\"tile_title\">Перемикач</string>\n    <string name=\"quick_toggle\">Переключити</string>\n    <string name=\"group_default\">Розгрупований</string>\n    <string name=\"document\">Документ</string>\n    <string name=\"theme\">Тема</string>\n    <string name=\"menu_about\">Про</string>\n    <string name=\"menu_group\">Група</string>\n    <string name=\"menu_configuration\">Конфігурація</string>\n    <string name=\"logcat_summary\">Такі корисні дуже вау</string>\n    <string name=\"logcat\">Експорт інформації про налагодження</string>\n    <string name=\"version_x\">Версія (%s)</string>\n    <string name=\"app_version\">Версія</string>\n    <string name=\"oss_licenses\">Ліцензії з відкритим кодом</string>\n    <string name=\"telegram\">Канал оновлення Telegram</string>\n    <string name=\"github\">Вихідний код</string>\n    <string name=\"project\">Проєкт</string>\n    <string name=\"app_desc\">Універсальний набір інструментів проксі для Android, написаний на Kotlin.</string>\n    <string name=\"route_name\">Назва маршруту</string>\n    <string name=\"delete_route_prompt\">Ви впевнені, що хочете видалити цей маршрут\\?</string>\n    <string name=\"route_add\">Створити маршрут</string>\n    <string name=\"menu_route\">Маршрут</string>\n    <string name=\"metered_summary\">Система підказок щодо розгляду VPN як лічильника</string>\n    <string name=\"metered\">Підказка з дозуванням</string>\n    <string name=\"only\">Тільки</string>\n    <string name=\"prefer\">Краще</string>\n    <string name=\"ipv6\">Маршрут IPv6</string>\n    <string name=\"edit_config\">Редагувати конфігурацію</string>\n    <string name=\"config_type\">Тип конфігурації</string>\n    <string name=\"extra_headers\">Додаткові заголовки</string>\n    <string name=\"encryption\">Шифрування</string>\n    <string name=\"early_data_header_name\">Назва раннього заголовка даних</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">Закріплений ланцюжок сертифікатів</string>\n    <string name=\"certificates\">Сертифікати</string>\n    <string name=\"alpn\">Переговори про протокол шару додатків</string>\n    <string name=\"sni\">Індикація імені сервера</string>\n    <string name=\"tls\">Використовуйте TLS</string>\n    <string name=\"grpc_service_name\">ім\\'я служби gRPC</string>\n    <string name=\"kcp_seed\">Насіння mKCP</string>\n    <string name=\"quic_key\">Швидкий КЛЮЧ</string>\n    <string name=\"quic_security\">QUIC Безпека</string>\n    <string name=\"http_path\">Шлях HTTP</string>\n    <string name=\"http_host\">Хост HTTP</string>\n    <string name=\"ws_path\">Шлях WebSocket</string>\n    <string name=\"ws_host\">Хост WebSocket</string>\n    <string name=\"header_type\">Тип заголовка</string>\n    <string name=\"network\">Мережа</string>\n    <string name=\"security\">Шифрування транспортного рівня</string>\n    <string name=\"alter_id\">Змінити ідентифікатор</string>\n    <string name=\"uuid\">ідентифікатор користувача</string>\n    <string name=\"protocol_param\">Протокол Param</string>\n    <string name=\"protocol\">Протокол</string>\n    <string name=\"enc_method\">Метод шифрування</string>\n    <string name=\"password\">Пароль</string>\n    <string name=\"key_opt\">Ключ (необов’язково)</string>\n    <string name=\"password_opt\">Пароль (необов’язково)</string>\n    <string name=\"username_opt\">Ім\\'я користувача (необов’язково)</string>\n    <string name=\"username\">Ім\\'я користувача</string>\n    <string name=\"server_port\">Віддалений порт</string>\n    <string name=\"server_address\">Сервер</string>\n    <string name=\"profile_name\">Імя профілю</string>\n    <string name=\"connection_test_url\">URL -адреса перевірки з\\'єднання</string>\n    <string name=\"transproxy_mode\">Режим Transproxy</string>\n    <string name=\"require_transproxy\">Увімкнути вхідний сигнал Transproxy</string>\n    <string name=\"obfs_param\">Obfs Парам</string>\n    <string name=\"obfs\">Obfs</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"menu_traffic\">трафіку</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"project\">项目</string>\n    <string name=\"github\">源代码</string>\n    <string name=\"telegram\">Telegram 更新频道</string>\n    <string name=\"oss_licenses\">开放源代码协议</string>\n    <string name=\"app_version\">版本</string>\n    <string name=\"logcat\">导出调试信息</string>\n    <string name=\"logcat_summary\">这种非常有用哇</string>\n    <string name=\"menu_configuration\">配置</string>\n    <string name=\"menu_group\">分组</string>\n    <string name=\"menu_about\">关于</string>\n    <string name=\"group_default\">未分组</string>\n    <string name=\"quick_toggle\">切换</string>\n    <!-- group -->\n    <string name=\"group_name\">分组名</string>\n    <string name=\"group_update\">更新</string>\n    <string name=\"group_subscription_link\">订阅链接</string>\n    <string name=\"group_name_required\">需要分组名称</string>\n    <string name=\"group_create\">创建分组</string>\n    <string name=\"group_create_subscription\">添加订阅</string>\n    <string name=\"group_edit\">编辑</string>\n    <string name=\"group_edit_subscription\">编辑订阅</string>\n    <string name=\"group_status_empty\">空</string>\n    <string name=\"group_status_empty_subscription\">从未更新</string>\n    <string name=\"group_status_proxies\">%d 个配置</string>\n    <string name=\"group_status_proxies_subscription\">%d 个配置 | 更新于 %s</string>\n    <string name=\"group_no_difference\">%s: 没有变化</string>\n    <string name=\"group_updated\">%s: 更新了 %d 个节点</string>\n    <string name=\"group_show_diff\">变化</string>\n    <string name=\"group_delete_confirm_prompt\">您确定要删除这个分组吗\\?</string>\n    <string name=\"group_added\">新增:\n\\n%s</string>\n    <string name=\"group_changed\">更新:\n\\n%s</string>\n    <string name=\"group_deleted\">删除:\n\\n%s</string>\n    <!-- misc -->\n    <string name=\"service_mode\">运行模式</string>\n    <string name=\"service_mode_proxy\">仅代理</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"port_proxy\">SOCKS5 代理端口</string>\n    <string name=\"port_http\">HTTP 代理端口</string>\n    <string name=\"port_local_dns\">本地 DNS 端口</string>\n    <string name=\"allow_access\">允许来自局域网的连接</string>\n    <string name=\"allow_access_sum\">将入站绑定至 0.0.0.0</string>\n    <string name=\"cag_dns\">DNS 设置</string>\n    <string name=\"cag_route\">路由设置</string>\n    <string name=\"inbound_settings\">入站设置</string>\n    <string name=\"general_settings\">软件设置</string>\n    <string name=\"require_http\">启用 HTTP 入站</string>\n    <string name=\"cag_ws\">WebSocket 设置</string>\n    <string name=\"ws_browser_forwarding\">浏览器转发</string>\n    <string name=\"ws_browser_forwarding_sum\">转发相应的 WebSockets 到浏览器.</string>\n    <string name=\"speed_interval\">速度通知更新间隔</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"speed_detail\">代理： %1$s↑ %2$s↓\n\\n直连： %3$s↑ %4$s↓</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"connection_test_testing\">测试中…</string>\n    <string name=\"connection_test_available\">连接成功: HTTPS 握手耗时 %dms</string>\n    <string name=\"connection_test_error\">失败: %s</string>\n    <string name=\"connection_test_fail\">网络不可用</string>\n    <string name=\"connection_test_error_status_code\">错误代码: #%d</string>\n    <!-- proxy category -->\n    <string name=\"profile_name\">配置名称</string>\n    <string name=\"server_address\">服务器</string>\n    <string name=\"server_port\">服务器端口</string>\n    <string name=\"username_opt\">用户名 (可选)</string>\n    <string name=\"password_opt\">密码 (可选)</string>\n    <string name=\"password\">密码</string>\n    <string name=\"enc_method\">加密方式</string>\n    <string name=\"protocol\">协议</string>\n    <string name=\"protocol_param\">协议参数</string>\n    <string name=\"obfs\">混淆</string>\n    <string name=\"obfs_param\">混淆参数</string>\n    <string name=\"uuid\">用户ID</string>\n    <string name=\"alter_id\">替代 ID</string>\n    <string name=\"security\">传输层加密</string>\n    <string name=\"network\">传输协议</string>\n    <string name=\"header_type\">伪装类型</string>\n    <string name=\"tls\">使用 TLS</string>\n    <string name=\"sni\">服务器名称指示</string>\n    <string name=\"encryption\">加密</string>\n    <!-- feature category -->\n    <string name=\"ipv6\">IPv6 路由</string>\n    <string name=\"metered\">按流量计费</string>\n    <string name=\"metered_summary\">让系统认为此 VPN 按流量计费</string>\n    <string name=\"menu_route\">路由</string>\n    <string name=\"proxied_apps\">分应用代理</string>\n    <string name=\"proxied_apps_summary\">代理被勾选应用的流量</string>\n    <string name=\"on\">打开</string>\n    <string name=\"off\">关闭</string>\n    <string name=\"search_apps\">搜索…</string>\n    <string name=\"scanning\">扫描中…</string>\n    <string name=\"invert_selections\">反选</string>\n    <string name=\"clear_selections\">清空</string>\n    <string name=\"bypass_apps\">绕过</string>\n    <string name=\"show_system_apps\">显示系统应用</string>\n    <string name=\"auto_connect\">自动连接</string>\n    <string name=\"auto_connect_summary\">手机启动或更新后代理会自动重新连接</string>\n    <string name=\"direct_boot_aware\">允许在解锁前启动</string>\n    <string name=\"direct_boot_aware_summary\">您正在使用的服务器可能会受到更少的保护</string>\n    <!-- notification category -->\n    <string name=\"service_vpn\">VPN 服务</string>\n    <string name=\"service_proxy\">代理服务</string>\n    <string name=\"forward_success\">代理已启动。</string>\n    <string name=\"invalid_server\">无效的服务器</string>\n    <string name=\"service_failed\">失败:</string>\n    <string name=\"stop\">停止</string>\n    <string name=\"stopping\">正在停止…</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"vpn_permission_denied\">VPN 服务权限请求被拒绝</string>\n    <string name=\"reboot_required\">无法启动 VPN 服务 , 您可能需要重启您的设备.</string>\n    <!-- alert category -->\n    <string name=\"profile_empty\">请选择一个服务器配置</string>\n    <string name=\"connect\">连接</string>\n    <string name=\"clipboard_empty\">剪切板没有内容</string>\n    <!-- menu category -->\n    <string name=\"settings\">设置</string>\n    <string name=\"edit\">编辑</string>\n    <string name=\"share\">分享</string>\n    <string name=\"add_profile\">添加服务器配置</string>\n    <string name=\"action_create_group\">空白组</string>\n    <string name=\"action_from_link\">添加订阅</string>\n    <string name=\"action_scan_china_apps\">扫描中国应用</string>\n    <string name=\"action_export_file\">导出到文件</string>\n    <string name=\"action_export_clipboard\">导出到剪切板</string>\n    <string name=\"action_import\">从剪切板导入</string>\n    <string name=\"action_import_file\">从文件中导入</string>\n    <string name=\"action_export_msg\">导出成功!</string>\n    <string name=\"action_export_err\">导出失败.</string>\n    <string name=\"action_import_msg\">导入成功!</string>\n    <string name=\"action_import_err\">导入失败.</string>\n    <!-- share -->\n    <!-- profile -->\n    <string name=\"profile_config\">服务器配置</string>\n    <string name=\"delete\">删除</string>\n    <string name=\"delete_confirm_prompt\">您确定要删除这个服务器配置吗\\?</string>\n    <string name=\"share_qr_nfc\">二维码</string>\n    <string name=\"add_profile_methods_scan_qr_code\">扫描二维码</string>\n    <string name=\"add_profile_methods_manual_settings\">手动输入</string>\n    <plurals name=\"removed\">\n        <item quantity=\"other\">已删除 %d 个配置</item>\n    </plurals>\n    <plurals name=\"added\">\n        <item quantity=\"other\">已添加 %d 个配置</item>\n    </plurals>\n    <string name=\"undo\">还原</string>\n    <!-- tasker -->\n    <!-- status -->\n    <string name=\"connecting\">正在连接…</string>\n    <string name=\"vpn_connected\">已连接 , 点击此处测试连接</string>\n    <string name=\"not_connected\">未连接</string>\n    <!-- subscriptions -->\n    <string name=\"subscriptions\">订阅</string>\n    <string name=\"cleartext_http_warning\">纯文本 HTTP 流量并不安全</string>\n    <string name=\"error_title\">错误</string>\n    <string name=\"no_proxies_found\">未在此链接内找到服务器配置</string>\n    <!-- plugin -->\n    <string name=\"plugin\">插件</string>\n    <string name=\"plugin_configure\">设置…</string>\n    <string name=\"plugin_disabled\">已停用</string>\n    <string name=\"plugin_unknown\">未知插件: %s</string>\n    <string name=\"plugin_untrusted\">请注意: 此插件的不来自可靠的来源.</string>\n    <string name=\"plugin_auto_connect_unlock_only\">此插件可能不支持自动连接</string>\n    <string name=\"proxy_cat\">服务器设置</string>\n    <string name=\"unsaved_changes_prompt\">是否要保存修改？</string>\n    <string name=\"yes\">是</string>\n    <string name=\"no\">否</string>\n    <string name=\"apply\">应用</string>\n    <string name=\"license\">授权协议</string>\n    <string name=\"route_proxy\">代理</string>\n    <string name=\"route_bypass\">绕过</string>\n    <string name=\"route_block\">屏蔽</string>\n    <string name=\"route_opt_bypass_lan\">绕过局域网地址</string>\n    <string name=\"route_opt_block_ads\">屏蔽广告</string>\n    <string name=\"domain_strategy\">域名解析策略</string>\n    <string name=\"traffic_sniffing\">启用流量探测</string>\n    <string name=\"disable\">禁用</string>\n    <string name=\"cag_misc\">杂项</string>\n    <string name=\"enable_mux\">启用多路复用</string>\n    <string name=\"mux_concurrency\">复用最大并发</string>\n    <string name=\"ws_host\">WebSocket 主机</string>\n    <string name=\"ws_path\">WebSocket 路径</string>\n    <string name=\"http_host\">HTTP 主机</string>\n    <string name=\"http_path\">HTTP 路径</string>\n    <string name=\"quic_security\">QUIC 加密方式</string>\n    <string name=\"quic_key\">QUIC 密钥</string>\n    <string name=\"kcp_seed\">mKCP 混淆密码</string>\n    <string name=\"grpc_service_name\">gRPC 服务名称</string>\n    <string name=\"alpn\">应用层协议协商</string>\n    <string name=\"group_duplicate\">重复:\n\\n%s</string>\n    <string name=\"deduplication\">去重</string>\n    <string name=\"version_x\">版本 (%s)</string>\n    <string name=\"flow\">流控</string>\n    <string name=\"show_stop\">显示停止按钮</string>\n    <string name=\"show_stop_sum\">如果您不想使用快捷图块</string>\n    <string name=\"ss_cat\">Shadowsocks 设置</string>\n    <string name=\"insecure\">不安全</string>\n    <string name=\"deprecated\">已废弃</string>\n    <string name=\"insecure_warn\">安全建议</string>\n    <string name=\"insecure_warn_sum\">由 Qv2ray 开发者社区提供</string>\n    <string name=\"show_direct_speed\">显示直连速度</string>\n    <string name=\"show_direct_speed_sum\">在通知中也显示不经过代理的流量速度</string>\n    <string name=\"select_profile\">选择配置</string>\n    <string name=\"proxy_chain\">链式代理</string>\n    <string name=\"chain_settings\">链设置</string>\n    <string name=\"circular_reference\">循环引用</string>\n    <string name=\"circular_reference_sum\">路由中不能包含自己.</string>\n    <string name=\"mux_sum\">多路复用是为了减少 TCP 的握手延迟而设计, 而非提高连接的吞吐量. 用于看视频、下载或者测速通常都有反效果</string>\n    <string name=\"mux_for_all\">为所有可能支持的协议启用多路复用</string>\n    <string name=\"mux_for_all_sum\">如果服务器不支持, 则将无法连接.</string>\n    <string name=\"tcp_keep_alive_interval\">TCP 保持活跃数据包发送间隔</string>\n    <string name=\"route_add\">新建路由规则</string>\n    <string name=\"delete_route_prompt\">您确定要移除此路由吗\\?</string>\n    <string name=\"route_name\">路由名称</string>\n    <string name=\"route_profile\">选择配置…</string>\n    <string name=\"empty_route\">空路由</string>\n    <string name=\"empty_route_notice\">在保存前设置一些规则</string>\n    <string name=\"route_warn\">在添加自定义规则之前，请确保您已经阅读了文档，否则您将可能无法连接到互联网。</string>\n    <string name=\"route_bypass_domain\">%s 域名规则</string>\n    <string name=\"route_bypass_ip\">%s IP 规则</string>\n    <string name=\"no_proxies_found_in_file\">未在此文件内找到服务器配置</string>\n    <string name=\"no_proxies_found_in_clipboard\">未在剪切板内找到服务器配置</string>\n    <string name=\"route_reset\">重置</string>\n    <string name=\"clear_profiles\">清空</string>\n    <string name=\"file_manager_missing\">您的设备缺少 Android 标准文件选择器, 请安装一个, 如 Material Files.</string>\n    <string name=\"restart\">重载代理服务以应用修改</string>\n    <string name=\"theme\">主题颜色</string>\n    <string name=\"donate\">捐款</string>\n    <string name=\"donate_info\">世界很可爱 请给世界钱</string>\n    <string name=\"extra_headers\">附加标头</string>\n    <string name=\"username\">用户名</string>\n    <string name=\"key_opt\">密码 (可选)</string>\n    <string name=\"group_diff\">变化 (%s)</string>\n    <string name=\"require_transproxy\">启用透明代理入站</string>\n    <string name=\"transproxy_mode\">透明代理模式</string>\n    <string name=\"port_transproxy\">透明代理端口</string>\n    <string name=\"connection_test_url\">连接测试链接</string>\n    <string name=\"connection_test_available_http\">连接成功: HTTP 握手耗时 %dms</string>\n    <string name=\"route_reverse\">反向代理</string>\n    <string name=\"route_reverse_redirect\">反向地址</string>\n    <string name=\"certificates\">证书 (链)</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">固定证书链散列</string>\n    <string name=\"ignore_battery_optimizations\">忽略电池优化</string>\n    <string name=\"ignore_battery_optimizations_sum\">移除一些限制</string>\n    <string name=\"document\">文档</string>\n    <string name=\"config_settings\">配置设置</string>\n    <string name=\"custom_config\">自定义配置</string>\n    <string name=\"multi_mode_sum\">这是一个实验性选项. 可能不会被长期保留, 也不保证跨版本兼容.</string>\n    <string name=\"xray_utls_fingerprint\">Xray uTLS 指纹</string>\n    <string name=\"night_mode\">夜间模式</string>\n    <string name=\"enable\">启用</string>\n    <string name=\"auto\">自动</string>\n    <string name=\"follow_system\">跟随系统</string>\n    <string name=\"security_settings\">安全设置</string>\n    <string name=\"allow_insecure\">允许不安全的连接</string>\n    <string name=\"allow_insecure_sum\">禁用证书检查. 启用后该配置安全性相当于明文</string>\n    <string name=\"config_type\">配置类型</string>\n    <string name=\"edit_config\">编辑配置</string>\n    <string name=\"lines\">%d 行</string>\n    <string name=\"only\">仅</string>\n    <string name=\"prefer\">优先</string>\n    <string name=\"route_not_asset\">不是资源文件: 预期 .dat 为扩展名, 但 %s</string>\n    <string name=\"route_asset_no_update\">已为最新</string>\n    <string name=\"route_asset_updated\">已更新</string>\n    <string name=\"route_asset_status\">本地版本: %s</string>\n    <string name=\"route_rules_provider\">路由资源更新源</string>\n    <string name=\"route_assets\">资源</string>\n    <string name=\"route_manage_assets\">管理路由资源</string>\n    <string name=\"enable_log\">启用日志</string>\n    <string name=\"enable_log_sum\">调试用途</string>\n    <string name=\"route_rules_official\">官方</string>\n    <string name=\"balancer\">负载均衡</string>\n    <string name=\"balancer_settings\">负载均衡设置</string>\n    <string name=\"balancer_type\">类型</string>\n    <string name=\"balancer_strategy\">策略</string>\n    <string name=\"list\">列表</string>\n    <string name=\"random\">随机</string>\n    <string name=\"api_port\">API 端口</string>\n    <string name=\"probe_interval\">负载均衡观测间隔</string>\n    <string name=\"standard\">标准</string>\n    <string name=\"unavailable\">不可用</string>\n    <string name=\"always_show_address\">始终显示地址</string>\n    <string name=\"always_show_address_sum\">始终在配置卡片上显示服务器地址</string>\n    <string name=\"clear_traffic_statistics\">清空流量统计数据</string>\n    <string name=\"connection_test\">连接测试</string>\n    <string name=\"connection_test_clear_results\">清理测试结果</string>\n    <string name=\"connection_test_domain_not_found\">域名不存在</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing 不可用</string>\n    <string name=\"connection_test_refused\">连接重置</string>\n    <string name=\"connection_test_timeout\">超时</string>\n    <string name=\"connection_test_unreachable\">不可达</string>\n    <string name=\"tile_title\">开关</string>\n    <string name=\"append_http_proxy\">追加 HTTP 代理至 VPN</string>\n    <string name=\"append_http_proxy_sum\">浏览器 / 一些支持的应用 将直接使用 HTTP 代理, 而不经过虚拟网卡设备 (Android 10+)</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing 不可用</string>\n    <string name=\"bypass_lan_in_core_only\">仅在 Core 中绕过局域网</string>\n    <string name=\"bypass_lan_in_core_only_sum\">如果 Lineage 之 \\\"允许热点客户端使用 VPN\\\" 无法使用, 尝试启用此项.</string>\n    <string name=\"protocol_settings\">协议设置</string>\n    <string name=\"trojan_provider\">Trojan 提供程序</string>\n    <string name=\"ss_aead_provider\">Shadowsocks AEAD 提供程序</string>\n    <string name=\"group_basic\">基本</string>\n    <string name=\"group_settings\">分组设置</string>\n    <string name=\"subscription\">订阅</string>\n    <string name=\"subscription_settings\">订阅设置</string>\n    <string name=\"group_type\">分组类型</string>\n    <string name=\"subscription_type\">订阅类型</string>\n    <string name=\"delete_group_prompt\">您确定要删除该分组吗 \\?</string>\n    <string name=\"force_resolve\">强制解析</string>\n    <string name=\"force_resolve_sum\">更新时解析所有域名到 IP 地址. 如果可能, 主机与服务器名称指示值将被自动覆盖</string>\n    <string name=\"force_vmess_aead\">强制 VMess AEAD</string>\n    <string name=\"force_vmess_aead_sum\">重写带有不安全的认证方式的 VMess 配置到 AEAD 认证</string>\n    <string name=\"deduplication_sum\">更新时移除重复配置</string>\n    <string name=\"raw\">原始</string>\n    <string name=\"update_settings\">更新设置</string>\n    <string name=\"auto_update\">自动更新</string>\n    <string name=\"auto_update_delay\">自动更新间隔 (分)</string>\n    <string name=\"update_when_connected_only\">仅在连接时更新</string>\n    <string name=\"update_when_connected_only_sum\">防止 IP 地址泄漏</string>\n    <string name=\"subscription_user_agent\">用户代理</string>\n    <string name=\"confirm\">确认</string>\n    <string name=\"missing_plugin\">缺少插件</string>\n    <string name=\"profile_requiring_plugin\">配置 %s 需要插件 %s, 但找不到.</string>\n    <string name=\"action_learn_more\">了解更多</string>\n    <string name=\"action_download\">下载</string>\n    <string name=\"install_from_play_store\">从 Play Store 安装</string>\n    <string name=\"install_from_fdroid\">从 F-Droid 安装</string>\n    <string name=\"download\">下载</string>\n    <string name=\"ooc_subscription_token_invalid\">无效的 OOCv1 Token</string>\n    <string name=\"ooc_warning\">警告</string>\n    <string name=\"ooc_missing_protocol\">该订阅要求 %s 协议支持, 但找不到. 不支持的配置将被忽略.</string>\n    <string name=\"service_subscription\">订阅更新服务</string>\n    <string name=\"subscription_update\">订阅更新</string>\n    <string name=\"subscription_update_message\">正更新 %s …</string>\n    <string name=\"update_subscription_warning\">代理未连接, 您确定要继续更新吗\\?</string>\n    <string name=\"group_filter\">筛选</string>\n    <string name=\"group_filter_groups\">组</string>\n    <string name=\"group_filter_tags\">标签</string>\n    <string name=\"group_filter_groups_nf\">该订阅中没有分组</string>\n    <string name=\"group_filter_tags_nf\">该订阅中没有标签</string>\n    <string name=\"group_filter_ns\">您需要一个 OOCv1 订阅以使用筛选功能</string>\n    <string name=\"subscription_used\">已用 %s</string>\n    <string name=\"subscription_traffic\">已用 %s / 剩余 %s</string>\n    <string name=\"subscription_import\">导入订阅</string>\n    <string name=\"subscription_import_message\">确认要导入订阅 %s？如果来源不可信, 您的 IP 和行为将被泄露.</string>\n    <string name=\"profile_import\">导入配置</string>\n    <string name=\"profile_import_message\">确认要导入配置 %s？</string>\n    <string name=\"no_proxies_found_in_subscription\">未在此订阅内找到服务器配置</string>\n    <string name=\"action_export\">导出</string>\n    <string name=\"clear_profiles_message\">您确定要清空该分组吗\\?</string>\n    <string name=\"remote_dns\">远程 DNS</string>\n    <string name=\"direct_dns\">直连 DNS</string>\n    <string name=\"enable_dns_routing\">启用 DNS 路由</string>\n    <string name=\"dns_routing_message\">使用直连 DNS 解析绕过路由中的 domains. 注意潜在的 DNS 泄漏</string>\n    <string name=\"enable_fakedns\">启用 FakeDNS</string>\n    <string name=\"fakedns_message\">可能导致其他应用程序在代理停止后需要重新启动以重新连接到网络</string>\n    <string name=\"dns_hosts\">域名重写</string>\n    <string name=\"apps\">应用</string>\n    <string name=\"select_apps\">选择应用</string>\n    <string name=\"apps_message\">%d 个应用</string>\n    <string name=\"hysteria_obfs\">混淆密码</string>\n    <string name=\"hysteria_auth_type\">认证类型</string>\n    <string name=\"hysteria_auth_payload\">认证载荷</string>\n    <string name=\"hysteria_upload_mbps\">最大上行 (Mbps)</string>\n    <string name=\"hysteria_download_mbps\">最大下行 (Mbps)</string>\n    <string name=\"experimental_settings\">实验性</string>\n    <string name=\"ss_stream_provider\">Shadowsocks 流式密码提供程序</string>\n    <string name=\"hysteria_stream_receive_window\">QUIC 流接收窗口</string>\n    <string name=\"hysteria_connection_receive_window\">QUIC 连接接收窗口</string>\n    <string name=\"hysteria_disable_mtu_discovery\">禁用路径最大传输单元发现</string>\n    <string name=\"group_order\">排序</string>\n    <string name=\"group_order_origin\">原始</string>\n    <string name=\"group_order_by_name\">以名称</string>\n    <string name=\"group_order_by_delay\">以延时</string>\n    <string name=\"plugin_exists_but_on_shit_system\">配置 %s 需要插件 %s，但你的专有设备供应商（通常也是监视资本主义巨头和恶意软件制造商）篡改了你的安卓系统，使该插件无法使用。</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray （Shadowsocks Android 插件）</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Simple Obfs （Shadowsocks Android 插件）</string>\n    <string name=\"experimental_no_termination_signal\">通过 VMess 传输时，不要为TCP连接发送连接单双工终止信号。这将破坏与一些应用程序的兼容性。</string>\n    <string name=\"experimental_authenticated_length\">让每个有效载荷段的长度不再具有可塑性。这个实验要求服务器和客户端使用相同版本的 v2ray-core。预计会有更多关于这个实验的破坏性更新。</string>\n    <string name=\"menu_traffic\">流量</string>\n    <string name=\"route_for\">%s 路由</string>\n    <string name=\"app_no_launcher\">该应用没有界面。</string>\n    <string name=\"copy_failed\">复制失败。</string>\n    <string name=\"copy_success\">复制成功！</string>\n    <string name=\"create_rule\">创建路由</string>\n    <string name=\"open_market\">打开应用市场</string>\n    <string name=\"open_settings\">打开设置</string>\n    <string name=\"open_app\">打开应用</string>\n    <string name=\"copy_package_name\">复制包名</string>\n    <string name=\"copy_label\">复制应用名</string>\n    <string name=\"traffic_holder\">启用 VPN 以统计流量</string>\n    <string name=\"udp_connections\">%d UDP 连接</string>\n    <string name=\"tcp_connections\">%d TCP 连接</string>\n    <string name=\"traffic_stats\">统计</string>\n    <string name=\"traffic_active\">活动</string>\n    <string name=\"route_need_fds\">路由规则 %s 依赖前台检测服务以生效，但它未被启用。</string>\n    <string name=\"route_need_vpn\">路由规则 %s 依赖 VPN 以生效，所以其已被忽略。</string>\n    <string name=\"background\">后台</string>\n    <string name=\"foreground\">前台</string>\n    <string name=\"foreground_detector_summary\">本服务用于支持基于前台状态的路由规则，如果未启用，则结果永远为后台。</string>\n    <string name=\"foreground_status\">前台状态</string>\n    <string name=\"foreground_detector\">前台检测器</string>\n    <string name=\"no_statistics\">暂无数据</string>\n    <string name=\"traffic_statistics_summary\">可能增加电量使用</string>\n    <string name=\"ssh_private_key_passphrase\">私钥密码句</string>\n    <string name=\"ssh_private_key\">私钥</string>\n    <string name=\"ssh_public_key\">公钥</string>\n    <string name=\"ssh_auth_type_none\">无</string>\n    <string name=\"clear_logcat\">清空日志</string>\n    <string name=\"menu_log\">日志</string>\n    <string name=\"menu_tools\">工具</string>\n    <string name=\"translate_platform\">翻译平台</string>\n    <string name=\"wireguard_local_address\">本地地址</string>\n    <string name=\"wireguard_public_key\">节点公钥</string>\n    <string name=\"wireguard_psk\">节点预共享密钥</string>\n    <string name=\"cloudflare_wrap\">Warp</string>\n    <string name=\"resolve_destination_summary\">如果目标地址是一个域, 则根据 IPv6 策略传出.</string>\n    <string name=\"resolve_destination\">解析目标地址</string>\n    <string name=\"destination_override_summary\">使用探测的域来覆盖目标地址, 而不是仅用于路由</string>\n    <string name=\"destination_override\">覆盖目标地址</string>\n    <string name=\"tun_implementation\">TUN 实现</string>\n    <string name=\"generating\">正在生成…</string>\n    <string name=\"warp_generate\">生成配置</string>\n    <string name=\"warp_license\">CloudFlare Wrap 是一个免费的 WireGuard VPN 提供源. 使用它, 代表您同意服务条款: https://www.cloudflare.com/application/terms/</string>\n    <string name=\"profile_traffic_statistics_summary\">禁用后将不统计使用的流量</string>\n    <string name=\"app_statistics_disabled\">应用流量统计已禁用</string>\n    <string name=\"enable_pcap\">启用 Pcap</string>\n    <string name=\"pcap_summary\">保存流量到 .pcap 文件</string>\n    <string name=\"pcap_notice\">Pcap 文件将被保存到 %s</string>\n    <string name=\"app_traffic_statistics\">应用流量统计</string>\n    <string name=\"profile_traffic_statistics\">配置流量统计</string>\n    <string name=\"group_filter_owners\">拥有者</string>\n    <string name=\"group_filter_owners_nf\">该订阅中没有拥有者</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"project\">專案</string>\n    <string name=\"github\">原始碼</string>\n    <string name=\"telegram\">Telegram 更新頻道</string>\n    <string name=\"oss_licenses\">開放原始碼授權協定</string>\n    <string name=\"app_version\">版本</string>\n    <string name=\"logcat\">匯出 debug 所需資訊</string>\n    <string name=\"logcat_summary\">這種非常有用啊(^O^)／</string>\n    <string name=\"menu_configuration\">設定檔</string>\n    <string name=\"menu_group\">組</string>\n    <string name=\"menu_about\">關於</string>\n    <string name=\"quick_toggle\">切換</string>\n    <!-- group -->\n    <string name=\"group_name\">群組名稱</string>\n    <string name=\"group_update\">更新</string>\n    <string name=\"group_subscription_link\">訂閱連接</string>\n    <string name=\"group_name_required\">需要群組名稱</string>\n    <string name=\"group_create\">建立新組</string>\n    <string name=\"group_create_subscription\">建立新訂閱</string>\n    <string name=\"group_edit\">編輯</string>\n    <string name=\"group_edit_subscription\">編輯訂閱</string>\n    <string name=\"group_status_empty\">空組</string>\n    <string name=\"group_status_empty_subscription\">尚未更新</string>\n    <string name=\"group_status_proxies\">%d 個代理</string>\n    <string name=\"group_status_proxies_subscription\">%d 個代理 | 更新於 %s</string>\n    <string name=\"group_no_difference\">%s: 沒有變化</string>\n    <string name=\"group_updated\">%s: 更新了 %d 個代理</string>\n    <string name=\"group_show_diff\">變化</string>\n    <string name=\"group_delete_confirm_prompt\">您真的希望刪除這個群組嗎？</string>\n    <string name=\"group_added\">新增：\n\\n%s</string>\n    <string name=\"group_changed\">更新：\n\\n%s</string>\n    <string name=\"group_deleted\">刪除：\n\\n%s</string>\n    <!-- misc -->\n    <string name=\"service_mode\">執行模式</string>\n    <string name=\"service_mode_proxy\">代理</string>\n    <string name=\"service_mode_vpn\">VPN</string>\n    <string name=\"port_proxy\">SOCKS5 代理連接埠</string>\n    <string name=\"port_http\">HTTP 代理連接埠</string>\n    <string name=\"port_local_dns\">本地 DNS 連接埠</string>\n    <string name=\"allow_access\">允許來自區域網路的連線</string>\n    <string name=\"allow_access_sum\">繫結傳入伺服器至 0.0.0.0</string>\n    <string name=\"cag_dns\">DNS 設定</string>\n    <string name=\"cag_route\">路由設定</string>\n    <string name=\"inbound_settings\">傳入設定</string>\n    <string name=\"general_settings\">應用程式設定</string>\n    <string name=\"require_http\">啟用 HTTP 傳入伺服器</string>\n    <string name=\"cag_ws\">WebSocket 設定</string>\n    <string name=\"ws_browser_forwarding\">瀏覽器轉發</string>\n    <string name=\"ws_browser_forwarding_sum\">轉傳對應的 WebSocket 連線至瀏覽器。</string>\n    <string name=\"speed_interval\">速度通知更新間隔</string>\n    <string name=\"traffic\">%1$s↑ %2$s↓</string>\n    <string name=\"speed_detail\">代理： %1$s↑ %2$s↓\n\\n直連： %3$s↑ %4$s↓</string>\n    <string name=\"speed\">%s/s</string>\n    <string name=\"connection_test_testing\">測試中……</string>\n    <string name=\"connection_test_available\">成功：HTTPS 握手花費 %dms</string>\n    <string name=\"connection_test_error\">失敗：%s</string>\n    <string name=\"connection_test_fail\">網路不可用</string>\n    <string name=\"connection_test_error_status_code\">錯誤碼：#%d</string>\n    <!-- proxy category -->\n    <string name=\"profile_name\">設定檔名稱</string>\n    <string name=\"server_address\">伺服器</string>\n    <string name=\"server_port\">遠端連接埠</string>\n    <string name=\"username_opt\">使用者名稱（可選）</string>\n    <string name=\"password_opt\">密碼（可選）</string>\n    <string name=\"password\">密碼</string>\n    <string name=\"enc_method\">加密方式</string>\n    <string name=\"protocol\">設定</string>\n    <string name=\"protocol_param\">協定參數</string>\n    <string name=\"obfs\">混淆</string>\n    <string name=\"obfs_param\">混淆參數</string>\n    <string name=\"uuid\">使用者 ID</string>\n    <string name=\"alter_id\">替代 ID</string>\n    <string name=\"network\">網路</string>\n    <string name=\"header_type\">標頭類型</string>\n    <string name=\"tls\">使用 TLS</string>\n    <string name=\"sni\">伺服器名稱指示</string>\n    <string name=\"encryption\">加密</string>\n    <!-- feature category -->\n    <string name=\"metered\">計量付費提示</string>\n    <string name=\"metered_summary\">提示系統將 VPN 視為計量付費</string>\n    <string name=\"menu_route\">路由</string>\n    <string name=\"proxied_apps\">應用程式 VPN 模式</string>\n    <string name=\"proxied_apps_summary\">為被選取的應用程式設定 VPN 模式</string>\n    <string name=\"on\">啟用</string>\n    <string name=\"off\">停用</string>\n    <string name=\"search_apps\">搜尋…</string>\n    <string name=\"scanning\">掃描中……</string>\n    <string name=\"invert_selections\">反向選取</string>\n    <string name=\"clear_selections\">清除選取</string>\n    <string name=\"bypass_apps\">略過</string>\n    <string name=\"show_system_apps\">顯示系統應用</string>\n    <string name=\"auto_connect\">自動連線</string>\n    <string name=\"auto_connect_summary\">如果代理在開機或應用更新前正在運行則自動啓動</string>\n    <string name=\"direct_boot_aware\">允許與解鎖前啓動</string>\n    <string name=\"direct_boot_aware_summary\">您選取的設定檔將受到較少的保護</string>\n    <!-- notification category -->\n    <string name=\"service_vpn\">VPN 服務</string>\n    <string name=\"service_proxy\">代理服務</string>\n    <string name=\"forward_success\">AnXray 已啟動。</string>\n    <string name=\"invalid_server\">伺服器名稱無效</string>\n    <string name=\"stop\">停止</string>\n    <string name=\"stopping\">關閉中……</string>\n    <string name=\"vpn_error\">%s</string>\n    <string name=\"vpn_permission_denied\">建立 VPN 服務的權限被拒</string>\n    <string name=\"reboot_required\">無法啟動 VPN 服務，您可能需要重新啟動您的裝置。</string>\n    <!-- alert category -->\n    <string name=\"profile_empty\">請選擇一個設定檔</string>\n    <string name=\"connect\">連線</string>\n    <string name=\"clipboard_empty\">剪貼簿為空</string>\n    <!-- menu category -->\n    <string name=\"settings\">設定</string>\n    <string name=\"edit\">編輯</string>\n    <string name=\"share\">分享</string>\n    <string name=\"add_profile\">新增設定檔</string>\n    <string name=\"action_create_group\">空白群組</string>\n    <string name=\"action_from_link\">來自訂閱</string>\n    <string name=\"action_scan_china_apps\">掃描中國應用程式</string>\n    <string name=\"action_export_file\">匯出至檔案</string>\n    <string name=\"action_export_clipboard\">匯出至剪貼簿</string>\n    <string name=\"action_import\">自剪貼簿匯入</string>\n    <string name=\"action_import_file\">自檔案匯入</string>\n    <string name=\"action_export_msg\">匯出成功！</string>\n    <string name=\"action_export_err\">匯出失敗。</string>\n    <string name=\"action_import_msg\">匯入成功！</string>\n    <string name=\"action_import_err\">匯入失敗。</string>\n    <!-- share -->\n    <!-- profile -->\n    <string name=\"profile_config\">設定檔設定</string>\n    <string name=\"delete\">刪除</string>\n    <string name=\"delete_confirm_prompt\">您確定要刪除此設定檔嗎？</string>\n    <string name=\"share_qr_nfc\">QR 碼</string>\n    <string name=\"add_profile_methods_scan_qr_code\">掃描 QR 碼</string>\n    <string name=\"add_profile_methods_manual_settings\">手動設定</string>\n    <plurals name=\"removed\">\n        <item quantity=\"other\">已刪除 %d 項</item>\n    </plurals>\n    <plurals name=\"added\">\n        <item quantity=\"other\">已新增 %d 個配置</item>\n    </plurals>\n    <string name=\"undo\">復原</string>\n    <!-- tasker -->\n    <!-- status -->\n    <string name=\"connecting\">連線中……</string>\n    <string name=\"vpn_connected\">已連線，輕觸以檢查連線能力</string>\n    <string name=\"not_connected\">未連線</string>\n    <!-- subscriptions -->\n    <string name=\"subscriptions\">訂閱</string>\n    <string name=\"cleartext_http_warning\">純文字 HTTP 流量並不安全</string>\n    <string name=\"error_title\">錯誤</string>\n    <string name=\"no_proxies_found\">此連接不包含任何代理</string>\n    <!-- plugin -->\n    <string name=\"plugin\">外掛程式</string>\n    <string name=\"plugin_configure\">設定……</string>\n    <string name=\"plugin_disabled\">已停用</string>\n    <string name=\"plugin_unknown\">未知外掛程式 %s</string>\n    <string name=\"plugin_untrusted\">警告：此外掛程式並非來自可靠的可信來源。</string>\n    <string name=\"plugin_auto_connect_unlock_only\">此外掛程式可能不支援自動連線</string>\n    <string name=\"proxy_cat\">伺服器設定</string>\n    <string name=\"unsaved_changes_prompt\">要儲存變更嗎？</string>\n    <string name=\"yes\">是</string>\n    <string name=\"no\">否</string>\n    <string name=\"apply\">套用</string>\n    <string name=\"license\">授權協定</string>\n    <string name=\"document\">文檔</string>\n    <string name=\"early_data_header_name\">前置資料標頭</string>\n    <string name=\"pinned_peer_certificate_chain_sha256\">固定憑證鏈結雜湊</string>\n    <string name=\"certificates\">憑證（鏈結）</string>\n    <string name=\"alpn\">應用層協定協商</string>\n    <string name=\"grpc_service_name\">gRPC 服務名稱</string>\n    <string name=\"kcp_seed\">mKCP 混淆密碼</string>\n    <string name=\"quic_key\">QUIC 金鑰</string>\n    <string name=\"quic_security\">QUIC 安全</string>\n    <string name=\"http_path\">HTTP 路徑</string>\n    <string name=\"http_host\">HTTP 主機</string>\n    <string name=\"ws_path\">WebSocket 路徑</string>\n    <string name=\"ws_host\">WebSocket 主機</string>\n    <string name=\"security\">傳輸層安全性協定</string>\n    <string name=\"key_opt\">金鑰（選用）</string>\n    <string name=\"username\">使用者名稱</string>\n    <string name=\"connection_test_url\">連線測試 URL</string>\n    <string name=\"fakedns_message\">可能導致其他應用程式在代理停止後需要重新啟動以重新連線至網路</string>\n    <string name=\"transproxy_mode\">透明代理模式</string>\n    <string name=\"require_transproxy\">啟用透明代理傳入伺服器</string>\n    <string name=\"dns_hosts\">網域重寫</string>\n    <string name=\"enable_fakedns\">啟用 FakeDNS</string>\n    <string name=\"dns_routing_message\">使用在路由中被略過的直連 DNS 解析網域。請注意潛在的 DNS 洩漏</string>\n    <string name=\"enable_dns_routing\">啟用 DNS 路由</string>\n    <string name=\"direct_dns\">直連 DNS</string>\n    <string name=\"remote_dns\">遠端 DNS</string>\n    <string name=\"connection_test_available_http\">成功：HTTP 握手花費 %d 毫秒</string>\n    <string name=\"allow_insecure_sum\">停用憑證檢查。當啟用時，此設定檔將和純文字同等安全</string>\n    <string name=\"allow_insecure\">允許不安全的連線</string>\n    <string name=\"security_settings\">安全設定</string>\n    <string name=\"show_direct_speed_sum\">在通知中一併顯示未被代理的流量速度</string>\n    <string name=\"show_direct_speed\">顯示直連速度</string>\n    <string name=\"show_stop_sum\">如果您不想使用快速設定圖塊進行切換</string>\n    <string name=\"show_stop\">顯示停止按鈕</string>\n    <string name=\"cag_misc\">其他設定</string>\n    <string name=\"port_transproxy\">透明代理連接埠</string>\n    <string name=\"group_duplicate\">重複：\n\\n%s</string>\n    <string name=\"group_diff\">差異（%s）</string>\n    <string name=\"tile_title\">切換器</string>\n    <string name=\"group_default\">未分組</string>\n    <string name=\"version_x\">版本 (%s)</string>\n    <string name=\"extra_headers\">額外標頭</string>\n    <string name=\"config_type\">設定類型</string>\n    <string name=\"theme\">主題</string>\n    <string name=\"edit_config\">編輯設定</string>\n    <string name=\"ipv6\">IPv6 路由</string>\n    <string name=\"prefer\">優先</string>\n    <string name=\"only\">僅</string>\n    <string name=\"route_add\">建立路由規則</string>\n    <string name=\"delete_route_prompt\">您真的希望移除此路由規則嗎？</string>\n    <string name=\"route_name\">路由名稱</string>\n    <string name=\"route_proxy\">代理</string>\n    <string name=\"route_bypass\">繞過</string>\n    <string name=\"route_block\">封鎖</string>\n    <string name=\"route_profile\">選擇設定檔…</string>\n    <string name=\"empty_route\">空白路由規則</string>\n    <string name=\"empty_route_notice\">在儲存前設定一些規則</string>\n    <string name=\"route_bypass_domain\">%s 網域規則</string>\n    <string name=\"route_bypass_ip\">%s IP 規則</string>\n    <string name=\"route_reset\">重設</string>\n    <string name=\"route_reverse_redirect\">反向位址</string>\n    <string name=\"route_reverse\">反向代理</string>\n    <string name=\"route_manage_assets\">管理路由資源</string>\n    <string name=\"route_assets\">資源</string>\n    <string name=\"route_rules_provider\">規則資源提供者</string>\n    <string name=\"route_rules_official\">官方</string>\n    <string name=\"route_asset_status\">本地版本：%s</string>\n    <string name=\"route_asset_no_update\">無更新</string>\n    <string name=\"route_asset_updated\">已更新</string>\n    <string name=\"route_not_asset\">非資源檔案：預期副檔名為 .dat，但 %s</string>\n    <string name=\"route_opt_bypass_lan\">略過區域網路位址</string>\n    <string name=\"route_opt_block_ads\">封鎖廣告</string>\n    <string name=\"domain_strategy\">網域解析策略</string>\n    <string name=\"traffic_sniffing\">啟用流量嗅探</string>\n    <string name=\"enable_mux\">啟用多工器</string>\n    <string name=\"mux_sum\">多工器被設計用於減少 TCP 握手延遲，而非提高連線的輸送量。使用多工器觀看影片，進行下載或進行網路速度測試通常都有反效果</string>\n    <string name=\"mux_concurrency\">多工器同時連線數目</string>\n    <string name=\"mux_for_all\">為所有可能的支援的協定啟用多工器</string>\n    <string name=\"mux_for_all_sum\">如果伺服器並不支援多工器，我們可能會無法成功連接它。</string>\n    <string name=\"tcp_keep_alive_interval\">TCP 保持連線傳送間隔</string>\n    <string name=\"service_failed\">失敗：</string>\n    <string name=\"select_profile\">選擇設定檔</string>\n    <string name=\"proxy_chain\">代理鏈結</string>\n    <string name=\"custom_config\">客製化設定檔</string>\n    <string name=\"balancer\">負載平衡器</string>\n    <string name=\"balancer_settings\">負載平衡器設定</string>\n    <string name=\"balancer_type\">類型</string>\n    <string name=\"balancer_strategy\">策略</string>\n    <string name=\"chain_settings\">鏈結設定</string>\n    <string name=\"config_settings\">設定檔設定</string>\n    <string name=\"circular_reference\">循環參考</string>\n    <string name=\"circular_reference_sum\">路由中不能包含自己。</string>\n    <string name=\"clear_profiles\">清除</string>\n    <string name=\"action_export\">匯出</string>\n    <string name=\"insecure_warn\">安全性建議</string>\n    <string name=\"insecure_warn_sum\">由 Qv2ray 開發者社群提供</string>\n    <string name=\"insecure\">不安全</string>\n    <string name=\"deprecated\">已被取代</string>\n    <string name=\"no_proxies_found_in_subscription\">未在此訂閱中找到代理</string>\n    <string name=\"no_proxies_found_in_file\">未在此檔案中找到代理</string>\n    <string name=\"no_proxies_found_in_clipboard\">未在剪貼簿中找到代理</string>\n    <string name=\"deduplication\">重複資料刪除</string>\n    <string name=\"donate\">捐贈</string>\n    <string name=\"ignore_battery_optimizations\">忽略電池效能最佳化</string>\n    <string name=\"ignore_battery_optimizations_sum\">移除一些限制</string>\n    <string name=\"ss_cat\">Shadowsocks 設定</string>\n    <string name=\"restart\">重新載入代理服務以套用修改</string>\n    <string name=\"route_warn\">請在新增客製化規則前閱讀文件，否則您可能會無法連線至網際網路。</string>\n    <string name=\"lines\">%d 行</string>\n    <string name=\"night_mode\">夜間模式</string>\n    <string name=\"follow_system\">跟隨系統</string>\n    <string name=\"enable\">啟用</string>\n    <string name=\"disable\">停用</string>\n    <string name=\"auto\">自動</string>\n    <string name=\"enable_log\">啟用記錄檔</string>\n    <string name=\"enable_log_sum\">用於除錯用途</string>\n    <string name=\"list\">列表</string>\n    <string name=\"random\">隨機</string>\n    <string name=\"api_port\">API 連接埠</string>\n    <string name=\"probe_interval\">負載平衡器觀測間隔</string>\n    <string name=\"standard\">標準</string>\n    <string name=\"unavailable\">無法使用</string>\n    <string name=\"always_show_address\">始終顯示位址</string>\n    <string name=\"always_show_address_sum\">使用在設定檔卡片中顯示伺服器位址</string>\n    <string name=\"clear_traffic_statistics\">清除流量統計資料</string>\n    <string name=\"connection_test\">連線測試</string>\n    <string name=\"connection_test_tcp_ping_unavailable\">TCPing 無法使用</string>\n    <string name=\"connection_test_clear_results\">清除測試結果</string>\n    <string name=\"connection_test_icmp_ping_unavailable\">ICMPing 無法使用</string>\n    <string name=\"connection_test_domain_not_found\">網域不存在</string>\n    <string name=\"connection_test_refused\">連線被拒</string>\n    <string name=\"donate_info\">世界很可愛 請給世界錢</string>\n    <string name=\"connection_test_unreachable\">無法連線</string>\n    <string name=\"connection_test_timeout\">逾時</string>\n    <string name=\"append_http_proxy\">為 VPN 附加 HTTP 代理</string>\n    <string name=\"append_http_proxy_sum\">HTTP 代理將直接被支援的瀏覽器/應用程式使用，而無需經過虛擬網路卡（Android 10+）</string>\n    <string name=\"bypass_lan_in_core_only\">僅在 Core 中繞過區域網路</string>\n    <string name=\"bypass_lan_in_core_only_sum\">如果 LineageOS 的“允許熱點客戶端使用此裝置的 VPN”無法使用，嘗試啟用此選項。</string>\n    <string name=\"ss_aead_provider\">Shadowsocks AEAD 提供程式</string>\n    <string name=\"trojan_provider\">Trojan 提供程式</string>\n    <string name=\"protocol_settings\">協議設定</string>\n    <string name=\"group_basic\">基本</string>\n    <string name=\"group_settings\">群組設定</string>\n    <string name=\"subscription\">訂閱</string>\n    <string name=\"subscription_settings\">訂閱設定</string>\n    <string name=\"group_type\">群組類型</string>\n    <string name=\"subscription_type\">訂閱類型</string>\n    <string name=\"delete_group_prompt\">您真的希望移除此群組嗎？</string>\n    <string name=\"force_resolve\">強制解析</string>\n    <string name=\"force_resolve_sum\">在更新時將所有網域解析為 IP。主機名和伺服器名稱指示將在可能時被自動附加</string>\n    <string name=\"force_vmess_aead\">強制 VMess AEAD</string>\n    <string name=\"force_vmess_aead_sum\">重寫使用不安全驗證方式的 VMess 設定檔至 AEAD 驗證（alterId 0）</string>\n    <string name=\"deduplication_sum\">在更新時移除重複的設定檔</string>\n    <string name=\"raw\">原始</string>\n    <string name=\"update_settings\">更新設定</string>\n    <string name=\"auto_update\">自動更新</string>\n    <string name=\"auto_update_delay\">自動更新間隔（分鐘）</string>\n    <string name=\"update_when_connected_only\">僅在已連線時更新</string>\n    <string name=\"update_when_connected_only_sum\">防止 IP 位址洩漏</string>\n    <string name=\"subscription_user_agent\">使用者代理</string>\n    <string name=\"confirm\">確認</string>\n    <string name=\"missing_plugin\">遺失外掛程式</string>\n    <string name=\"profile_requiring_plugin\">設定檔 %s 需要外掛程式 %s，但其未被安裝。</string>\n    <string name=\"action_learn_more\">深入瞭解</string>\n    <string name=\"action_download\">下載</string>\n    <string name=\"install_from_play_store\">從 Play 商店安裝</string>\n    <string name=\"install_from_fdroid\">從 F-Droid 安裝</string>\n    <string name=\"download\">下載</string>\n    <string name=\"ooc_subscription_token_invalid\">無效的 OOCv1 權杖</string>\n    <string name=\"update_subscription_warning\">代理尚未連線，您真的希望繼續更新嗎？</string>\n    <string name=\"ooc_warning\">警告</string>\n    <string name=\"ooc_missing_protocol\">此訂閱需要 %s 協定支援，但其未被安裝，不支援的設定檔將被忽略。</string>\n    <string name=\"service_subscription\">訂閱更新服務</string>\n    <string name=\"subscription_update\">訂閱更新</string>\n    <string name=\"subscription_update_message\">更新 %s 中…</string>\n    <string name=\"group_filter\">篩選</string>\n    <string name=\"group_filter_groups\">群組</string>\n    <string name=\"group_filter_tags\">標籤</string>\n    <string name=\"group_filter_ns\">需要 OOCv1 訂閱以使用篩選功能</string>\n    <string name=\"group_filter_groups_nf\">此訂閱中沒有群組</string>\n    <string name=\"group_filter_tags_nf\">此訂閱中沒有標籤</string>\n    <string name=\"subscription_used\">已用 %s</string>\n    <string name=\"subscription_traffic\">已用 %s / 剩餘 %s</string>\n    <string name=\"subscription_import\">匯入訂閱</string>\n    <string name=\"subscription_import_message\">您確定要匯入訂閱 %s 嗎？如果訂閱的來源不可信，您的 IP 和行為將被洩漏。</string>\n    <string name=\"profile_import\">匯入設定檔</string>\n    <string name=\"profile_import_message\">您確定要匯入設定檔 %s 嗎？</string>\n    <string name=\"clear_profiles_message\">您確定要清除此群組嗎？</string>\n    <string name=\"apps\">應用程式</string>\n    <string name=\"select_apps\">選擇應用程式</string>\n    <string name=\"apps_message\">%d 個應用程式</string>\n    <string name=\"file_manager_missing\">您的裝置缺少 Android 標準檔案選擇器, 請安裝一個, 如 Material Files.</string>\n    <string name=\"menu_traffic\">流量</string>\n    <string name=\"shadowsocks_plugin_v2ray\">V2Ray (Shadowsocks Android 外掛程式)</string>\n    <string name=\"shadowsocks_plugin_simple_obfs\">Simple Obfs (Shadowsocks Android 外掛程式)</string>\n    <string name=\"plugin_exists_but_on_shit_system\">設定檔 %s 需要外掛程式 %s，但是您的專有設備提供商（通常也是監視資本主義巨頭和惡意軟體製造商）篡改了你的 Android 系統，使此外掛程式不可用。</string>\n    <string name=\"ssh_private_key_passphrase\">私密金鑰複雜密碼</string>\n    <string name=\"ssh_private_key\">私密金鑰</string>\n    <string name=\"ssh_public_key\">公開金鑰</string>\n    <string name=\"ssh_auth_type_none\">無</string>\n    <string name=\"no_statistics\">暫無統計資料</string>\n    <string name=\"traffic_statistics_summary\">可能增加電量消耗</string>\n    <string name=\"route_need_fds\">路由規則 %s 需要前景模式偵測器以生效，但未被啟用。</string>\n    <string name=\"route_need_vpn\">路由規則 %s 需要 VPN 模式以生效，因此被忽略。</string>\n    <string name=\"background\">背景</string>\n    <string name=\"foreground\">前景</string>\n    <string name=\"foreground_status\">前景狀態</string>\n    <string name=\"foreground_detector_summary\">此服務為基於前景狀態的應用程式規則提供支援，如果它未被啟動，結果將永遠為背景。</string>\n    <string name=\"foreground_detector\">前景偵測器</string>\n    <string name=\"route_for\">%s 的規則</string>\n    <string name=\"app_no_launcher\">此應用程式沒有介面。</string>\n    <string name=\"copy_failed\">複製失敗。</string>\n    <string name=\"copy_success\">成功複製！</string>\n    <string name=\"create_rule\">建立規則</string>\n    <string name=\"open_market\">打開應用商店</string>\n    <string name=\"open_settings\">打開設定</string>\n    <string name=\"open_app\">打開應用程式</string>\n    <string name=\"copy_package_name\">複製軟體包名稱</string>\n    <string name=\"copy_label\">複製名稱</string>\n    <string name=\"traffic_holder\">打開 VPN 以記錄流量統計資料</string>\n    <string name=\"udp_connections\">%d 個 UDP 連線</string>\n    <string name=\"tcp_connections\">%d 個 TCP 連線</string>\n    <string name=\"traffic_stats\">統計資料</string>\n    <string name=\"traffic_active\">作用中</string>\n    <string name=\"group_order_by_delay\">以延遲</string>\n    <string name=\"group_order_by_name\">以名稱</string>\n    <string name=\"group_order_origin\">原始</string>\n    <string name=\"group_order\">順序</string>\n    <string name=\"hysteria_disable_mtu_discovery\">停用路徑 MTU 探索</string>\n    <string name=\"hysteria_connection_receive_window\">QUIC 連線接收視窗</string>\n    <string name=\"hysteria_stream_receive_window\">QUIC 串流接收視窗</string>\n    <string name=\"experimental_no_termination_signal\">不在使用 VMess 傳輸時為 TCP 連線傳送單雙工中止訊號。此選項和一些應用程式不相容。</string>\n    <string name=\"experimental_authenticated_length\">使每個認證承載的長度不再具有延展性。此實驗要求伺服器和客戶端使用相同版本的 v2ray-core。此實驗預期會有更多破壞性更新。</string>\n    <string name=\"experimental_settings\">實驗性</string>\n    <string name=\"hysteria_download_mbps\">最大下載速度（Mbps）</string>\n    <string name=\"hysteria_upload_mbps\">最大上傳速度（Mbps）</string>\n    <string name=\"hysteria_auth_payload\">認證承載</string>\n    <string name=\"hysteria_auth_type\">認證類型</string>\n    <string name=\"hysteria_obfs\">混淆密碼</string>\n    <string name=\"ss_stream_provider\">Shadowsocks 串流加密提供者</string>\n    <string name=\"menu_tools\">工具</string>\n    <string name=\"translate_platform\">翻譯平台</string>\n    <string name=\"menu_log\">記錄檔</string>\n    <string name=\"profile_traffic_statistics_summary\">當停用時，使用的流量將不會被統計</string>\n    <string name=\"app_statistics_disabled\">應用程式流量統計資料被停用</string>\n    <string name=\"tun_implementation\">TUN 實作</string>\n    <string name=\"destination_override\">覆寫目的地位址</string>\n    <string name=\"resolve_destination\">解析目的地位址</string>\n    <string name=\"clear_logcat\">清除記錄檔</string>\n    <string name=\"wireguard_local_address\">本機位址</string>\n    <string name=\"wireguard_public_key\">節點公開金鑰</string>\n    <string name=\"wireguard_psk\">節點預先共用金鑰</string>\n    <string name=\"warp_license\">Cloudflare Warp 是一個免費的 WireGuard VPN 提供者。使用此服務表示您同意其服務條款：https://www.cloudflare.com/application/terms/</string>\n    <string name=\"warp_generate\">產生設定檔</string>\n    <string name=\"generating\">正在產生……</string>\n    <string name=\"enable_pcap\">啟用 Pcap</string>\n    <string name=\"pcap_summary\">儲存流量資料至 .pcap 檔案</string>\n    <string name=\"pcap_notice\">Pcap 檔案將被儲存至 %s</string>\n    <string name=\"profile_traffic_statistics\">設定檔流量統計資料</string>\n    <string name=\"app_traffic_statistics\">應用程式流量統計資料</string>\n    <string name=\"resolve_destination_summary\">如果目的地位址是一個網域，則基於 IPv6 策略傳出。</string>\n    <string name=\"destination_override_summary\">使用被偵測到的網域複寫目的地位址，而不是僅用於路由</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/xml/backup_descriptor.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<full-backup-content xmlns:tools=\"http://schemas.android.com/tools\"\n                     tools:ignore=\"FullBackupContent\">\n    <include domain=\"database\" path=\"sager_net.db\"/>\n    <!-- No device storage yet in Android 6.0 -->\n    <include domain=\"database\" path=\"configuration.db\"/>\n    <include domain=\"device_database\" path=\"configuration.db\"/>\n</full-backup-content>\n"
  },
  {
    "path": "app/src/main/res/xml/balancer_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <com.takisoft.preferencex.SimpleMenuPreference\n        app:defaultValue=\"0\"\n        app:entries=\"@array/balancer_type\"\n        app:entryValues=\"@array/int_array_2\"\n        app:icon=\"@drawable/ic_baseline_nfc_24\"\n        app:key=\"balancerType\"\n        app:title=\"@string/balancer_type\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <com.takisoft.preferencex.SimpleMenuPreference\n        app:entries=\"@array/balancer_strategy_entry\"\n        app:entryValues=\"@array/balancer_strategy_value\"\n        app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n        app:key=\"balancerStrategy\"\n        app:title=\"@string/balancer_strategy\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <io.nekohasekai.sagernet.widget.GroupPreference\n        app:icon=\"@drawable/ic_baseline_view_list_24\"\n        app:key=\"balancerGroup\"\n        app:title=\"@string/menu_group\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/brook_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/brook_protocol_entry\"\n            app:entryValues=\"@array/brook_protocol_value\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"serverProtocol\"\n            app:title=\"@string/protocol\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_format_align_left_24\"\n            app:key=\"serverPath\"\n            app:title=\"@string/ws_path\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/config_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <com.takisoft.preferencex.SimpleMenuPreference\n        app:defaultValue=\"v2ray\"\n        app:entries=\"@array/config_type_entry\"\n        app:entryValues=\"@array/config_type_value\"\n        app:icon=\"@drawable/ic_baseline_nfc_24\"\n        app:key=\"serverProtocol\"\n        app:title=\"@string/config_type\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <io.nekohasekai.sagernet.widget.EditConfigPreference\n        app:icon=\"@drawable/ic_image_edit\"\n        app:key=\"serverConfig\"\n        app:title=\"@string/edit\" />\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/foreground_detector_service.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<accessibility-service xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:accessibilityEventTypes=\"typeWindowStateChanged\"\n    android:canRetrieveWindowContent=\"false\"\n    android:summary=\"@string/foreground_detector\"\n    android:isAccessibilityTool=\"true\"\n    android:settingsActivity=\"io.nekohasekai.sagernet.ui.MainActivity\"\n    android:description=\"@string/foreground_detector_summary\" />"
  },
  {
    "path": "app/src/main/res/xml/global_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <PreferenceCategory app:title=\"@string/general_settings\">\n        <SwitchPreference\n            app:defaultValue=\"true\"\n            app:icon=\"@drawable/ic_communication_phonelink_ring\"\n            app:key=\"isAutoConnect\"\n            app:summary=\"@string/auto_connect_summary\"\n            app:title=\"@string/auto_connect\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_action_lock\"\n            app:key=\"directBootAware\"\n            app:summary=\"@string/direct_boot_aware_summary\"\n            app:title=\"@string/direct_boot_aware\" />\n        <io.nekohasekai.sagernet.widget.ColorPickerPreference\n            android:title=\"@string/theme\"\n            app:icon=\"@drawable/ic_baseline_color_lens_24\"\n            app:key=\"appTheme\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/night_mode\"\n            app:entryValues=\"@array/int_array_4\"\n            app:icon=\"@drawable/ic_baseline_wb_sunny_24\"\n            app:key=\"nightTheme\"\n            app:title=\"@string/night_mode\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"vpn\"\n            app:entries=\"@array/service_modes\"\n            app:entryValues=\"@array/service_mode_values\"\n            app:icon=\"@drawable/ic_device_developer_mode\"\n            app:key=\"serviceMode\"\n            app:title=\"@string/service_mode\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/tun_implementation\"\n            app:entryValues=\"@array/int_array_2\"\n            app:icon=\"@drawable/ic_baseline_flip_camera_android_24\"\n            app:key=\"tunImplementation\"\n            app:title=\"@string/tun_implementation\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_manage_search_24\"\n            app:key=\"appTrafficStatistics\"\n            app:summary=\"@string/traffic_statistics_summary\"\n            app:title=\"@string/app_traffic_statistics\" />\n        <SwitchPreference\n            app:defaultValue=\"true\"\n            app:key=\"profileTrafficStatistics\"\n            app:summary=\"@string/profile_traffic_statistics_summary\"\n            app:title=\"@string/profile_traffic_statistics\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"1000\"\n            app:entries=\"@array/notification_entry\"\n            app:entryValues=\"@array/notification_value\"\n            app:icon=\"@drawable/ic_baseline_shutter_speed_24\"\n            app:key=\"speedInterval\"\n            app:title=\"@string/speed_interval\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_center_focus_weak_24\"\n            app:key=\"alwaysShowAddress\"\n            app:summary=\"@string/always_show_address_sum\"\n            app:title=\"@string/always_show_address\" />\n        <SwitchPreference\n            app:defaultValue=\"true\"\n            app:icon=\"@drawable/ic_baseline_security_24\"\n            app:key=\"securityAdvisory\"\n            app:summary=\"@string/insecure_warn_sum\"\n            app:title=\"@string/insecure_warn\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_device_data_usage\"\n            app:key=\"meteredNetwork\"\n            app:summary=\"@string/metered_summary\"\n            app:title=\"@string/metered\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_speed_24\"\n            app:key=\"showDirectSpeed\"\n            app:summary=\"@string/show_direct_speed_sum\"\n            app:title=\"@string/show_direct_speed\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_multiple_stop_24\"\n            app:key=\"showStopButton\"\n            app:summary=\"@string/show_stop_sum\"\n            app:title=\"@string/show_stop\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_nat_24\"\n            app:key=\"allowAccess\"\n            app:summary=\"@string/allow_access_sum\"\n            app:title=\"@string/allow_access\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_bug_report_24\"\n            app:key=\"enableLog\"\n            app:summary=\"@string/enable_log_sum\"\n            app:title=\"@string/enable_log\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory app:title=\"@string/cag_route\">\n        <SwitchPreference\n            app:icon=\"@drawable/ic_navigation_apps\"\n            app:key=\"proxyApps\"\n            app:summary=\"@string/proxied_apps_summary\"\n            app:title=\"@string/proxied_apps\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_legend_toggle_24\"\n            app:key=\"bypassLan\"\n            app:title=\"@string/route_opt_bypass_lan\" />\n        <SwitchPreference\n            android:summary=\"@string/bypass_lan_in_core_only_sum\"\n            app:key=\"bypassLanInCoreOnly\"\n            app:title=\"@string/bypass_lan_in_core_only\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"AsIs\"\n            app:entries=\"@array/domain_strategy\"\n            app:entryValues=\"@array/domain_strategy\"\n            app:icon=\"@drawable/ic_action_dns\"\n            app:key=\"domainStrategy\"\n            app:title=\"@string/domain_strategy\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:defaultValue=\"true\"\n            app:icon=\"@drawable/ic_baseline_manage_search_24\"\n            app:key=\"trafficSniffing\"\n            app:title=\"@string/traffic_sniffing\" />\n        <SwitchPreference\n            app:icon=\"@drawable/baseline_construction_24\"\n            app:key=\"destinationOverride\"\n            app:summary=\"@string/destination_override_summary\"\n            app:title=\"@string/destination_override\" />\n        <SwitchPreference\n            app:icon=\"@drawable/baseline_wrap_text_24\"\n            app:key=\"resolveDestination\"\n            app:summary=\"@string/resolve_destination_summary\"\n            app:title=\"@string/resolve_destination\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"1\"\n            app:entries=\"@array/ipv6_mode\"\n            app:entryValues=\"@array/int_array_4\"\n            app:icon=\"@drawable/ic_image_looks_6\"\n            app:key=\"ipv6Mode\"\n            app:title=\"@string/ipv6\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/rules_dat_provider\"\n            app:entryValues=\"@array/int_array_2\"\n            app:icon=\"@drawable/ic_baseline_rule_folder_24\"\n            app:key=\"rulesProvider\"\n            app:title=\"@string/route_rules_provider\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory app:title=\"@string/protocol_settings\">\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/ss_aead_provider\"\n            app:entryValues=\"@array/ss_aead_provider_values\"\n            app:icon=\"@drawable/ic_baseline_nfc_24\"\n            app:key=\"providerShadowsocksAEAD\"\n            app:title=\"@string/ss_aead_provider\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/ss_stream_provider\"\n            app:entryValues=\"@array/ss_stream_provider_values\"\n            app:key=\"providerShadowsocksStream\"\n            app:title=\"@string/ss_stream_provider\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/trojan_provider_experimental\"\n            app:entryValues=\"@array/int_array_3\"\n            app:key=\"providerTrojan\"\n            app:title=\"@string/trojan_provider\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory app:title=\"@string/cag_dns\">\n        <EditTextPreference\n            app:defaultValue=\"https://1.0.0.1/dns-query\"\n            app:icon=\"@drawable/ic_action_dns\"\n            app:key=\"remoteDns\"\n            app:title=\"@string/remote_dns\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:defaultValue=\"https+local://223.5.5.5/dns-query\"\n            app:key=\"directDns\"\n            app:title=\"@string/direct_dns\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:defaultValue=\"domain:googleapis.cn googleapis.com\"\n            app:icon=\"@drawable/ic_baseline_transform_24\"\n            app:key=\"dnsHosts\"\n            app:title=\"@string/dns_hosts\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_camera_24\"\n            app:key=\"enableDnsRouting\"\n            app:summary=\"@string/dns_routing_message\"\n            app:title=\"@string/enable_dns_routing\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_action_lock\"\n            app:key=\"enableFakeDns\"\n            app:summary=\"@string/fakedns_message\"\n            app:title=\"@string/enable_fakedns\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory app:title=\"@string/inbound_settings\">\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"socksPort\"\n            app:title=\"@string/port_proxy\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:key=\"portLocalDns\"\n            app:title=\"@string/port_local_dns\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_http_24\"\n            app:key=\"requireHttp\"\n            app:title=\"@string/require_http\" />\n        <EditTextPreference\n            app:key=\"httpPort\"\n            app:title=\"@string/port_http\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:defaultValue=\"true\"\n            app:key=\"appendHttpProxy\"\n            app:summary=\"@string/append_http_proxy_sum\"\n            app:title=\"@string/append_http_proxy\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_transgender_24\"\n            app:key=\"requireTransproxy\"\n            app:title=\"@string/require_transproxy\" />\n        <EditTextPreference\n            app:key=\"transproxyPort\"\n            app:title=\"@string/port_transproxy\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"0\"\n            app:entries=\"@array/transproxy_mode\"\n            app:entryValues=\"@array/int_array_2\"\n            app:key=\"transproxyMode\"\n            app:title=\"@string/transproxy_mode\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory app:title=\"@string/cag_misc\">\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"\"\n            app:entries=\"@array/utls_entry\"\n            app:entryValues=\"@array/utls_value\"\n            app:icon=\"@drawable/ic_baseline_fingerprint_24\"\n            app:key=\"utlsFingerprint\"\n            app:title=\"@string/utls_fingerprint\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <io.nekohasekai.sagernet.widget.LinkPreference\n            app:defaultValue=\"https://api.v2fly.org/checkConnection.svgz\"\n            app:icon=\"@drawable/ic_baseline_cast_connected_24\"\n            app:key=\"connectionTestURL\"\n            app:title=\"@string/connection_test_url\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:defaultValue=\"false\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"enableMux\"\n            app:summary=\"@string/mux_sum\"\n            app:title=\"@string/enable_mux\" />\n        <SwitchPreference\n            app:defaultValue=\"false\"\n            app:icon=\"@drawable/ic_baseline_multiline_chart_24\"\n            app:key=\"enableMuxForAll\"\n            app:summary=\"@string/mux_for_all_sum\"\n            app:title=\"@string/mux_for_all\" />\n        <EditTextPreference\n            app:defaultValue=\"8\"\n            app:icon=\"@drawable/ic_baseline_low_priority_24\"\n            app:key=\"muxConcurrency\"\n            app:title=\"@string/mux_concurrency\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:defaultValue=\"15\"\n            app:icon=\"@drawable/ic_baseline_flip_camera_android_24\"\n            app:key=\"tcpKeepAliveInterval\"\n            app:title=\"@string/tcp_keep_alive_interval\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/baseline_save_24\"\n            app:key=\"enablePcap\"\n            app:summary=\"@string/pcap_summary\"\n            app:title=\"@string/enable_pcap\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/group_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"groupName\"\n        app:title=\"@string/group_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <com.takisoft.preferencex.SimpleMenuPreference\n        app:defaultValue=\"0\"\n        app:entries=\"@array/group_types\"\n        app:entryValues=\"@array/int_array_2\"\n        app:icon=\"@drawable/ic_baseline_layers_24\"\n        app:key=\"groupType\"\n        app:title=\"@string/group_type\"\n        app:useSimpleSummaryProvider=\"true\" />\n    <com.takisoft.preferencex.SimpleMenuPreference\n        app:defaultValue=\"0\"\n        app:entries=\"@array/group_orders\"\n        app:entryValues=\"@array/int_array_3\"\n        app:icon=\"@drawable/ic_baseline_low_priority_24\"\n        app:key=\"groupOrder\"\n        app:title=\"@string/group_order\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory\n        app:key=\"groupSubscription\"\n        app:title=\"@string/subscription_settings\">\n\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/subscription_types\"\n            app:entryValues=\"@array/int_array_3\"\n            app:icon=\"@drawable/ic_baseline_nfc_24\"\n            app:key=\"subscriptionType\"\n            app:title=\"@string/subscription_type\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <io.nekohasekai.sagernet.widget.LinkOrContentPreference\n            app:icon=\"@drawable/ic_baseline_link_24\"\n            app:key=\"subscriptionLink\"\n            app:title=\"@string/group_subscription_link\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <io.nekohasekai.sagernet.widget.OOCv1TokenPreference\n            app:icon=\"@drawable/ic_baseline_vpn_key_24\"\n            app:key=\"subscriptionToken\"\n            app:title=\"@string/ooc_subscription_token\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_manage_search_24\"\n            app:key=\"subscriptionForceResolve\"\n            app:summary=\"@string/force_resolve_sum\"\n            app:title=\"@string/force_resolve\" />\n\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_import_contacts_24\"\n            app:key=\"subscriptionDeduplication\"\n            app:summary=\"@string/deduplication_sum\"\n            app:title=\"@string/deduplication\" />\n\n        <SwitchPreference\n            app:defaultValue=\"true\"\n            app:icon=\"@drawable/ic_baseline_legend_toggle_24\"\n            app:key=\"subscriptionForceVMessAEAD\"\n            app:summary=\"@string/force_vmess_aead_sum\"\n            app:title=\"@string/force_vmess_aead\" />\n\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:key=\"subscriptionUpdate\"\n        app:title=\"@string/update_settings\">\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_security_24\"\n            app:key=\"subscriptionUpdateWhenConnectedOnly\"\n            app:summary=\"@string/update_when_connected_only_sum\"\n            app:title=\"@string/update_when_connected_only\" />\n        <io.nekohasekai.sagernet.widget.UserAgentPreference\n            app:icon=\"@drawable/ic_baseline_grid_3x3_24\"\n            app:key=\"subscriptionUserAgent\"\n            app:title=\"@string/subscription_user_agent\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_flip_camera_android_24\"\n            app:key=\"subscriptionAutoUpdate\"\n            app:title=\"@string/auto_update\" />\n        <EditTextPreference\n            app:defaultValue=\"1440\"\n            app:icon=\"@drawable/ic_baseline_timelapse_24\"\n            app:key=\"subscriptionAutoUpdateDelay\"\n            app:title=\"@string/auto_update_delay\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/http_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_person_24\"\n            app:key=\"serverUsername\"\n            app:title=\"@string/username_opt\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password_opt\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_https_24\"\n            app:key=\"serverTLS\"\n            app:title=\"@string/tls\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverSNI\"\n            app:title=\"@string/sni\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/hysteria_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverObfs\"\n            app:title=\"@string/hysteria_obfs\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/hysteria_auth_type\"\n            app:entryValues=\"@array/int_array_3\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"serverAuthType\"\n            app:title=\"@string/hysteria_auth_type\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/hysteria_auth_payload\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverSNI\"\n            app:title=\"@string/sni\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_vpn_key_24\"\n            app:key=\"serverCertificates\"\n            app:title=\"@string/certificates\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverAllowInsecure\"\n            app:summary=\"@string/allow_insecure_sum\"\n            app:title=\"@string/allow_insecure\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_file_file_upload\"\n            app:key=\"serverUploadSpeed\"\n            app:title=\"@string/hysteria_upload_mbps\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_download_24\"\n            app:key=\"serverDownloadSpeed\"\n            app:title=\"@string/hysteria_download_mbps\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverStreamReceiveWindow\"\n            app:title=\"@string/hysteria_stream_receive_window\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_transform_24\"\n            app:key=\"serverConnectionReceiveWindow\"\n            app:title=\"@string/hysteria_connection_receive_window\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_multiple_stop_24\"\n            app:key=\"serverDisableMtuDiscovery\"\n            app:title=\"@string/hysteria_disable_mtu_discovery\" />\n    </PreferenceCategory>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/log_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <cache-path name=\"log\" path=\"log/\"/>\n</paths>\n"
  },
  {
    "path": "app/src/main/res/xml/naive_preferences.xml",
    "content": "<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_person_24\"\n            app:key=\"serverUsername\"\n            app:title=\"@string/username_opt\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password_opt\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            android:layout_height=\"match_parent\"\n            app:defaultValue=\"https\"\n            app:entries=\"@array/naive_proto_entry\"\n            app:entryValues=\"@array/naive_proto_value\"\n            app:icon=\"@drawable/ic_baseline_layers_24\"\n            app:key=\"serverProtocol\"\n            app:title=\"@string/protocol\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverHeaders\"\n            app:title=\"@string/extra_headers\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/name_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\"/>\n</network-security-config>\n"
  },
  {
    "path": "app/src/main/res/xml/pingtunnel_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/key_opt\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/relaybaton_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_person_24\"\n            app:key=\"serverUsername\"\n            app:title=\"@string/username\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/route_preferences.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"routeName\"\n        app:title=\"@string/route_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n    <io.nekohasekai.sagernet.widget.AppListPreference\n        app:icon=\"@drawable/ic_baseline_legend_toggle_24\"\n        app:key=\"routePackages\"\n        app:title=\"@string/apps\" />\n    <com.takisoft.preferencex.SimpleMenuPreference\n        app:entries=\"@array/foreground_status\"\n        app:entryValues=\"@array/foreground_status_value\"\n        app:icon=\"@drawable/ic_baseline_low_priority_24\"\n        app:key=\"routeForegroundStatus\"\n        app:title=\"@string/foreground_status\"\n        app:useSimpleSummaryProvider=\"true\" />\n    <PreferenceCategory app:title=\"@string/cag_route\">\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_domain_24\"\n            app:key=\"routeDomain\"\n            app:title=\"domain\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_add_road_24\"\n            app:key=\"routeIP\"\n            app:title=\"ip\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"routePort\"\n            app:title=\"port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_home_24\"\n            app:key=\"routeSourcePort\"\n            app:title=\"sourcePort\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/route_protocol_entry\"\n            app:entryValues=\"@array/route_protocol_value\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"routeNetwork\"\n            app:title=\"network\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_local_bar_24\"\n            app:key=\"routeSource\"\n            app:title=\"source\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_layers_24\"\n            app:key=\"routeProtocol\"\n            app:title=\"protocol\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_manage_search_24\"\n            app:key=\"routeAttrs\"\n            app:title=\"attrs\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <io.nekohasekai.sagernet.widget.OutboundPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"routeOutbound\"\n            app:title=\"outbound\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_domain_24\"\n            app:key=\"routeReverse\"\n            app:title=\"@string/route_reverse\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_flip_camera_android_24\"\n            app:key=\"routeRedirect\"\n            app:title=\"@string/route_reverse_redirect\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/shadowsocks_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/enc_method_entry\"\n            app:entryValues=\"@array/enc_method_value\"\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverMethod\"\n            app:title=\"@string/enc_method\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:title=\"@string/plugin\">\n\n        <com.github.shadowsocks.preference.PluginPreference\n            app:key=\"serverPlugin\"\n            app:persistent=\"false\"\n            app:title=\"@string/plugin\"\n            app:useSimpleSummaryProvider=\"true\"/>\n        <EditTextPreference\n            app:key=\"serverPluginConfigure\"\n            app:icon=\"@drawable/ic_action_settings\"\n            app:persistent=\"false\"\n            app:title=\"@string/plugin_configure\"\n            app:useSimpleSummaryProvider=\"true\"/>\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/shadowsocksr_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/enc_method_entry_ssr\"\n            app:entryValues=\"@array/enc_method_value_ssr\"\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverMethod\"\n            app:title=\"@string/enc_method\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/protocol_entry\"\n            app:entryValues=\"@array/protocol_value\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"serverProtocol\"\n            app:title=\"@string/protocol\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_format_align_left_24\"\n            app:key=\"serverProtocolParam\"\n            app:title=\"@string/protocol_param\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/obfs_entry\"\n            app:entryValues=\"@array/obfs_value\"\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverObfs\"\n            app:title=\"@string/obfs\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_format_align_left_24\"\n            app:key=\"serverObfsParam\"\n            app:title=\"@string/obfs_param\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <shortcut\n        android:icon=\"@drawable/ic_qu_shadowsocks_launcher\"\n        android:shortcutId=\"toggle\"\n        android:shortcutLongLabel=\"@string/quick_toggle\"\n        android:shortcutShortLabel=\"@string/quick_toggle\">\n        <intent\n            android:action=\"android.intent.action.MAIN\"\n            android:targetClass=\"io.nekohasekai.sagernet.QuickToggleShortcut\"\n            android:targetPackage=\"io.nekohasekai.anXray\" />\n    </shortcut>\n    <shortcut\n        android:icon=\"@drawable/ic_qu_camera_launcher\"\n        android:shortcutId=\"scan\"\n        android:shortcutLongLabel=\"@string/add_profile_methods_scan_qr_code\"\n        android:shortcutShortLabel=\"@string/add_profile_methods_scan_qr_code\">\n        <intent\n            android:action=\"android.intent.action.MAIN\"\n            android:targetClass=\"io.nekohasekai.sagernet.ui.ScannerActivity\"\n            android:targetPackage=\"io.nekohasekai.anXray\" />\n    </shortcut>\n</shortcuts>\n"
  },
  {
    "path": "app/src/main/res/xml/snell_preferences.xml",
    "content": "<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            android:layout_height=\"match_parent\"\n            app:defaultValue=\"1\"\n            app:entries=\"@array/snell_versions\"\n            app:entryValues=\"@array/snell_versions\"\n            app:icon=\"@drawable/ic_baseline_layers_24\"\n            app:key=\"serverProtocol\"\n            app:title=\"@string/app_version\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/simple_obfs\"\n            app:entryValues=\"@array/simple_obfs\"\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverObfs\"\n            app:title=\"@string/obfs\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_format_align_left_24\"\n            app:key=\"serverObfsParam\"\n            app:title=\"@string/http_host\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/socks_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"2\"\n            app:entries=\"@array/socks_versions\"\n            app:entryValues=\"@array/int_array_3\"\n            app:icon=\"@drawable/ic_baseline_nfc_24\"\n            app:key=\"serverProtocol\"\n            app:title=\"@string/app_version\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_person_24\"\n            app:key=\"serverUsername\"\n            app:title=\"@string/username_opt\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password_opt\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_https_24\"\n            app:key=\"serverTLS\"\n            app:title=\"@string/tls\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverSNI\"\n            app:title=\"@string/sni\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/ssh_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_person_24\"\n            app:key=\"serverUsername\"\n            app:title=\"@string/username\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/ssh_auth_type\"\n            app:entryValues=\"@array/int_array_3\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"serverAuthType\"\n            app:title=\"@string/hysteria_auth_type\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_vpn_key_24\"\n            app:key=\"serverPrivateKey\"\n            app:title=\"@string/ssh_private_key\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword1\"\n            app:title=\"@string/ssh_private_key_passphrase\" />\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverCertificates\"\n            app:title=\"@string/ssh_public_key\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/standard_v2ray_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_baseline_person_24\"\n            app:key=\"serverUserId\"\n            app:title=\"@string/uuid\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_360\"\n            app:key=\"serverAlterId\"\n            app:title=\"@string/alter_id\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/vmess_encryption_entry\"\n            app:entryValues=\"@array/vmess_encryption_value\"\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverEncryption\"\n            app:title=\"@string/encryption\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/networks_entry\"\n            app:entryValues=\"@array/networks_value\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"serverNetwork\"\n            app:title=\"@string/network\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"none\"\n            app:entries=\"@array/kcp_quic_headers_entry\"\n            app:entryValues=\"@array/kcp_quic_headers_value\"\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverHeader\"\n            app:title=\"@string/header_type\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"none\"\n            app:entries=\"@array/quic_security_entry\"\n            app:entryValues=\"@array/quic_security_value\"\n            app:icon=\"@drawable/ic_baseline_security_24\"\n            app:key=\"serverQuicSecurity\"\n            app:title=\"@string/quic_security\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_airplanemode_active_24\"\n            app:key=\"serverHost\"\n            app:title=\"@string/http_host\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_format_align_left_24\"\n            app:key=\"serverPath\"\n            app:title=\"@string/http_path\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_running_with_errors_24\"\n            app:key=\"serverMultiMode\"\n            app:summary=\"@string/multi_mode_sum\"\n            app:title=\"@string/multi_mode\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/transport_layer_encryption_entry\"\n            app:entryValues=\"@array/transport_layer_encryption_value\"\n            app:icon=\"@drawable/ic_baseline_layers_24\"\n            app:key=\"serverSecurity\"\n            app:title=\"@string/security\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:key=\"serverSecurityCategory\"\n        app:title=\"@string/security_settings\">\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverSNI\"\n            app:title=\"@string/sni\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_legend_toggle_24\"\n            app:key=\"serverALPN\"\n            app:title=\"@string/alpn\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_vpn_key_24\"\n            app:key=\"serverCertificates\"\n            app:title=\"@string/certificates\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/xtls_flow_entry\"\n            app:entryValues=\"@array/xtls_flow_value\"\n            app:icon=\"@drawable/ic_baseline_stream_24\"\n            app:key=\"serverFlow\"\n            app:title=\"@string/flow\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverAllowInsecure\"\n            app:summary=\"@string/allow_insecure_sum\"\n            app:title=\"@string/allow_insecure\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:key=\"serverWsCategory\"\n        app:title=\"@string/cag_ws\">\n\n        <SwitchPreference\n            app:icon=\"@drawable/ic_baseline_texture_24\"\n            app:key=\"serverWsBrowserForwarding\"\n            app:summary=\"@string/ws_browser_forwarding_sum\"\n            app:title=\"@string/ws_browser_forwarding\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:icon=\"@drawable/ic_baseline_grid_3x3_24\"\n        app:key=\"serverVMessExperimentsCategory\"\n        app:title=\"@string/experimental_settings\">\n        <SwitchPreference\n            app:key=\"serverVMessExperimentalAuthenticatedLength\"\n            app:summary=\"@string/experimental_authenticated_length\"\n            app:title=\"AuthenticatedLength\" />\n        <SwitchPreference\n            app:key=\"serverVMessExperimentalNoTerminationSignal\"\n            app:summary=\"@string/experimental_no_termination_signal\"\n            app:title=\"NoTerminationSignal\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/trojan_go_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverSNI\"\n            app:title=\"@string/sni\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"none\"\n            app:entries=\"@array/trojan_go_networks_entry\"\n            app:entryValues=\"@array/trojan_go_networks_value\"\n            app:icon=\"@drawable/ic_baseline_compare_arrows_24\"\n            app:key=\"serverNetwork\"\n            app:title=\"@string/network\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/trojan_go_security_entry\"\n            app:entryValues=\"@array/trojan_go_security_value\"\n            app:icon=\"@drawable/ic_baseline_layers_24\"\n            app:key=\"serverEncryption\"\n            app:title=\"@string/encryption\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:key=\"serverWsCategory\"\n        app:title=\"@string/cag_ws\">\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_airplanemode_active_24\"\n            app:key=\"serverHost\"\n            app:title=\"@string/ws_host\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_format_align_left_24\"\n            app:key=\"serverPath\"\n            app:title=\"@string/ws_path\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        app:key=\"serverSsCategory\"\n        app:title=\"@string/ss_cat\">\n\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:defaultValue=\"AES-128-GCM\"\n            app:entries=\"@array/trojan_go_methods\"\n            app:entryValues=\"@array/trojan_go_methods\"\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverMethod\"\n            app:title=\"@string/enc_method\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword1\"\n            app:title=\"@string/password\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory app:title=\"@string/plugin\">\n\n        <com.github.shadowsocks.preference.PluginPreference\n            app:key=\"serverPlugin\"\n            app:persistent=\"false\"\n            app:title=\"@string/plugin\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_settings\"\n            app:key=\"serverPluginConfigure\"\n            app:persistent=\"false\"\n            app:title=\"@string/plugin_configure\"\n            app:useSimpleSummaryProvider=\"true\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/trojan_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/password\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/trojan_transport_layer_encryption_entry\"\n            app:entryValues=\"@array/trojan_transport_layer_encryption_value\"\n            app:icon=\"@drawable/ic_baseline_layers_24\"\n            app:key=\"serverSecurity\"\n            app:title=\"@string/security\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverSNI\"\n            app:title=\"@string/sni\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_legend_toggle_24\"\n            app:key=\"serverALPN\"\n            app:title=\"@string/alpn\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <com.takisoft.preferencex.SimpleMenuPreference\n            app:entries=\"@array/xtls_flow_entry\"\n            app:entryValues=\"@array/xtls_flow_value\"\n            app:icon=\"@drawable/ic_baseline_stream_24\"\n            app:key=\"serverFlow\"\n            app:title=\"@string/flow\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <SwitchPreference\n            app:icon=\"@drawable/ic_notification_enhanced_encryption\"\n            app:key=\"serverAllowInsecure\"\n            app:summary=\"@string/allow_insecure_sum\"\n            app:title=\"@string/allow_insecure\" />\n    </PreferenceCategory>\n\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/wireguard_preferences.xml",
    "content": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <EditTextPreference\n        app:icon=\"@drawable/ic_social_emoji_symbols\"\n        app:key=\"profileName\"\n        app:title=\"@string/profile_name\"\n        app:useSimpleSummaryProvider=\"true\" />\n\n    <PreferenceCategory app:title=\"@string/proxy_cat\">\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_domain_24\"\n            app:key=\"serverLocalAddress\"\n            app:title=\"@string/wireguard_local_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_baseline_vpn_key_24\"\n            app:key=\"serverPrivateKey\"\n            app:title=\"@string/ssh_private_key\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_hardware_router\"\n            app:key=\"serverAddress\"\n            app:title=\"@string/server_address\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:icon=\"@drawable/ic_maps_directions_boat\"\n            app:key=\"serverPort\"\n            app:title=\"@string/server_port\"\n            app:useSimpleSummaryProvider=\"true\" />\n\n        <EditTextPreference\n            app:icon=\"@drawable/ic_action_copyright\"\n            app:key=\"serverCertificates\"\n            app:title=\"@string/wireguard_public_key\"\n            app:useSimpleSummaryProvider=\"true\" />\n        <EditTextPreference\n            app:dialogLayout=\"@layout/layout_password_dialog\"\n            app:icon=\"@drawable/ic_settings_password\"\n            app:key=\"serverPassword\"\n            app:title=\"@string/wireguard_psk\" />\n    </PreferenceCategory>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/test/java/io/nekohasekai/sagernet/ExampleUnitTest.kt",
    "content": "package io.nekohasekai.sagernet\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "app/src/test/java/io/nekohasekai/sagernet/fmt/v2ray/TestParseV2Ray.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.fmt.v2ray\n\nimport io.nekohasekai.sagernet.ktx.applyDefaultValues\nimport junit.framework.TestCase\n\nclass TestParseV2Ray : TestCase() {\n\n    fun testParseV2rayNv1() {\n\n        val address = \"42.255.255.254\"\n        val alterId = 4\n        val uuid = \"59f34e8c-f310-49b0-b240-11663e365601\"\n        val network = \"tcp\"\n        val port = 11451\n        val comment = \"日本 VIP节点5 - 10Mbps带宽 苏州-日本 IPLC-CEN专线 游戏加速用 30倍流量比例 原生日本IP落地\"\n\n\n        val vmess = parseV2RayN(\n            \"vmess://eyJhZGQiOiI0Mi4yNTUuMjU1LjI1NCIsImFpZCI6NCwiaWQiOiI1OWYzNGU4Yy1mMzEw\" +\n                    \"LTQ5YjAtYjI0MC0xMTY2M2UzNjU2MDEiLCJuZXQiOiJ0Y3AiLCJwb3J0IjoxMTQ1MSwicHMiOiLm\" +\n                    \"l6XmnKwgVklQ6IqC54K5NSAtIDEwTWJwc+W4puWuvSDoi4/lt54t5pel5pysIElQTEMtQ0VO5LiT\" +\n                    \"57q/IOa4uOaIj+WKoOmAn+eUqCAzMOWAjea1gemHj+avlOS+iyDljp/nlJ/ml6XmnKxJUOiQveWc\" +\n                    \"sCIsInRscyI6Im5vbmUiLCJ0eXBlIjoibm9uZSIsInYiOjJ9Cg==\"\n        )\n\n        assertEquals(address, vmess.serverAddress)\n        assertEquals(alterId, vmess.alterId)\n        assertEquals(uuid, vmess.uuid)\n        assertEquals(network, vmess.type)\n        assertEquals(port, vmess.serverPort)\n        assertEquals(comment, vmess.name)\n\n    }\n\n    fun testParseV2rayNv2() {\n\n        val address = \"42.255.255.254\";\n        val alterId = 4;\n        val uuid = \"59f34e8c-f310-49b0-b240-11663e365601\";\n        val network = \"tcp\";\n        val port = 11451;\n        val comment = \"日本 VIP节点5 - 10Mbps带宽 苏州-日本 IPLC-CEN专线 游戏加速用 30倍流量比例 原生日本IP落地\";\n\n        val vmess =\n            parseV2RayN(\"vmess://eyJhZGQiOiI0Mi4yNTUuMjU1LjI1NCIsImFpZCI6NCwiaWQiOiI1OWYzNGU4Yy1mMzEw\" +\n                    \"LTQ5YjAtYjI0MC0xMTY2M2UzNjU2MDEiLCJuZXQiOiJ0Y3AiLCJwb3J0IjoxMTQ1MSwicHMiOiLm\" +\n                    \"l6XmnKwgVklQ6IqC54K5NSAtIDEwTWJwc+W4puWuvSDoi4/lt54t5pel5pysIElQTEMtQ0VO5LiT\" +\n                    \"57q/IOa4uOaIj+WKoOmAn+eUqCAzMOWAjea1gemHj+avlOS+iyDljp/nlJ/ml6XmnKxJUOiQveWc\" +\n                    \"sCIsInRscyI6Im5vbmUiLCJ0eXBlIjoibm9uZSIsInYiOjJ9Cg==\")\n\n        assertEquals(address, vmess.serverAddress)\n        assertEquals(alterId, vmess.alterId)\n        assertEquals(uuid, vmess.uuid)\n        assertEquals(network, vmess.type)\n        assertEquals(port, vmess.serverPort)\n        assertEquals(comment, vmess.name)\n\n        vmess.initializeDefaultValues()\n        assertEquals(parseV2RayN(vmess.toV2rayN()).applyDefaultValues(), vmess)\n\n    }\n\n    fun testParseVLESSgrpc() {\n        val vless =\n            parseV2Ray(\"vless://6d76fa31-8de2-40d4-8fee-6e61339c416f@qv2ray.net:123?type=grpc&security=tls&serviceName=FuckGFW\")\n\n        assertEquals(vless.serverAddress, \"qv2ray.net\")\n        assertEquals(vless.serverPort, 123)\n        assertEquals(vless.uuid, \"6d76fa31-8de2-40d4-8fee-6e61339c416f\")\n        assertEquals(vless.security, \"tls\")\n        assertEquals(vless.type, \"grpc\")\n        assertEquals(vless.grpcServiceName, \"FuckGFW\")\n\n        vless.initializeDefaultValues()\n        assertEquals(parseV2Ray(vless.toUri()).applyDefaultValues(), vless)\n    }\n\n}"
  },
  {
    "path": "app/src/test/java/io/nekohasekai/sagernet/ktx/UUIDsKtTest.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\n * Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com>                          *\n * Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be>  *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.ktx\n\nimport junit.framework.TestCase\n\nclass UUIDsKtTest : TestCase() {\n\n    fun testGenUUID5() {\n        val uuid = uuid5(\"example\")\n        print(uuid)\n        assertEquals(uuid, \"feb54431-301b-52bb-a6dd-e1e93e81bb9e\")\n    }\n\n}"
  },
  {
    "path": "bin/fdroid/build.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\n\nbin/lib/core/build.sh\n"
  },
  {
    "path": "bin/fdroid/install_golang.sh",
    "content": "#!/bin/bash\n\ncurl -o golang.tar.gz https://storage.googleapis.com/golang/go1.17.linux-amd64.tar.gz\nmkdir \"$HOME/.go\"\ntar -C \"$HOME/.go\" --strip-components=1 -xzf golang.tar.gz\nrm golang.tar.gz\nexport PATH=\"$PATH:$HOME/.go/bin\"\ngo version || exit 1"
  },
  {
    "path": "bin/fdroid/prebuild.sh",
    "content": "#/bin/bash\n\nsource \"bin/init/env.sh\"\n\ngit submodule update --init 'external/*'\ngit submodule update --init 'library/*'\n\n## Install rust\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain none -y\necho \"source \\$HOME/.cargo/env\" >>$HOME/.bashrc\nsource $HOME/.cargo/env\npushd library/shadowsocks/src/main/rust/shadowsocks-rust\nrustup install $(cat rust-toolchain)\nrustup default $(cat rust-toolchain)\nrustup target install armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android\npopd\n\necho \"rust.rustcCommand=$HOME/.cargo/bin/rustc\" >>local.properties\necho \"rust.cargoCommand=$HOME/.cargo/bin/cargo\" >>local.properties\necho \"rust.pythonCommand=/usr/bin/python3\" >>local.properties\n\nbin/fdroid/install_golang.sh\n\necho \"sdk.dir=$ANDROID_HOME\" >>local.properties\necho \"ndk.dir=$ANDROID_NDK_HOME\" >>local.properties\n\nbin/lib/core/init.sh\n"
  },
  {
    "path": "bin/fdroid/prebuild_plugin_golang.sh",
    "content": "#!/bin/bash\n\ngit submodule update --init \"plugin/$1\"\n\nbin/fdroid/install_golang.sh\n"
  },
  {
    "path": "bin/fdroid/prebuild_plugin_naive.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\n\ngit submodule update --init 'plugin/naive/*'\n\nif [ ! -x \"$HOME/.local/lib/git/bin/git\" ]; then\n\n  curl -L -o git.tar.gz https://www.kernel.org/pub/software/scm/git/git-2.33.0.tar.gz &&\n    mkdir -p git &&\n    tar -C git --strip-components=1 -xzf git.tar.gz &&\n    pushd git &&\n    make configure &&\n    ./configure \\\n      --with-curl \\\n      --prefix=$HOME/.local/lib/git &&\n    make install -j16 &&\n    popd &&\n    rm -rf git.tar.gz git\n\nfi\n"
  },
  {
    "path": "bin/init/action/library.sh",
    "content": "#!/bin/bash\n\ngit submodule update --init 'library/core/*'\ngit submodule update --init 'external/*'\n"
  },
  {
    "path": "bin/init/action/naive.sh",
    "content": "#!/usr/bin/env bash\n\nsudo apt-get install -y ninja-build pkg-config\nsudo apt-get remove -y libc6-i386 --autoremove\n"
  },
  {
    "path": "bin/init/action/shadowsocks.sh",
    "content": "#!/usr/bin/env bash\n\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.#rustup.rs | sh -s -- --default-toolchain none -y\necho \"source \\$HOME/.cargo/env\" >>$HOME/.bashrc\n\ngit submodule update --init library/shadowsocks/src/main/rust/shadowsocks-rust\ncd library/shadowsocks/src/main/rust/shadowsocks-rust\nrustup target install armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android\n\n# rustup default $(cat rust-toolchain)"
  },
  {
    "path": "bin/init/env.sh",
    "content": "#!/bin/bash\n\nif [ -z \"$ANDROID_HOME\" ]; then\n  if [ -d \"$HOME/Android/Sdk\" ]; then\n    export ANDROID_HOME=\"$HOME/Android/Sdk\"\n  elif [ -d \"$HOME/.local/lib/android/sdk\" ]; then\n    export ANDROID_HOME=\"$HOME/.local/lib/android/sdk\"\n  elif [ -d \"$HOME/Library/Android/sdk\" ]; then\n    export ANDROID_HOME=\"$HOME/Library/Android/sdk\"\n  fi\nfi\n\n_NDK=\"$ANDROID_HOME/ndk/21.4.7075529\"\n[ -f \"$_NDK/source.properties\" ] || _NDK=\"$ANDROID_NDK_HOME\"\n[ -f \"$_NDK/source.properties\" ] || _NDK=\"$NDK\"\n[ -f \"$_NDK/source.properties\" ] || _NDK=\"$ANDROID_HOME/ndk-bundle\"\n\nif [ ! -f \"$_NDK/source.properties\" ]; then\n  read -p \"Enter your NDK version: \" NDK_VERSION\n  _NDK=\"$ANDROID_HOME/ndk/$NDK_VERSION\"\nfi\n\nif [ ! -f \"$_NDK/source.properties\" ]; then\n  echo \"Error: NDK not found.\"\n  exit 1\nfi\n\nexport ANDROID_NDK_HOME=$_NDK\nexport NDK=$_NDK\n\nif [[ \"$OSTYPE\" =~ ^darwin ]]; then\n  export PROJECT=$PWD\nelse\n  export PROJECT=$(realpath .)\nfi\n\nif [ ! $(command -v go) ]; then\n  if [ -d /usr/lib/go-1.16 ]; then\n    export PATH=\"$PATH:/usr/lib/go-1.16/bin\"\n  elif [ -d $HOME/.go ]; then\n    export PATH=\"$PATH:$HOME/.go/bin\"\n  fi\nfi\n\nif [ $(command -v go) ]; then\n  export PATH=\"$PATH:$(go env GOPATH)/bin\"\nfi\n\nDEPS=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin\n\nexport ANDROID_ARM_CC=$DEPS/armv7a-linux-androideabi16-clang\nexport ANDROID_ARM_CXX=$DEPS/armv7a-linux-androideabi16-clang++\nexport ANDROID_ARM_CC_21=$DEPS/armv7a-linux-androideabi21-clang\nexport ANDROID_ARM_CXX_21=$DEPS/armv7a-linux-androideabi21-clang++\nexport ANDROID_ARM_STRIP=$DEPS/arm-linux-androideabi-strip\n\nexport ANDROID_ARM64_CC=$DEPS/aarch64-linux-android21-clang\nexport ANDROID_ARM64_CXX=$DEPS/aarch64-linux-android21-clang++\nexport ANDROID_ARM64_STRIP=$DEPS/aarch64-linux-android-strip\n\nexport ANDROID_X86_CC=$DEPS/i686-linux-android16-clang\nexport ANDROID_X86_CXX=$DEPS/i686-linux-android16-clang++\nexport ANDROID_X86_CC_21=$DEPS/i686-linux-android21-clang\nexport ANDROID_X86_CXX_21=$DEPS/i686-linux-android21-clang++\nexport ANDROID_X86_STRIP=$DEPS/i686-linux-android-strip\n\nexport ANDROID_X86_64_CC=$DEPS/x86_64-linux-android21-clang\nexport ANDROID_X86_64_CXX=$DEPS/x86_64-linux-android21-clang++\nexport ANDROID_X86_64_STRIP=$DEPS/x86_64-linux-android-strip\n"
  },
  {
    "path": "bin/lib/core/build.sh",
    "content": "#!/usr/bin/env bash\n\nsource \"bin/init/env.sh\"\nexport CGO_ENABLED=1\nexport GO386=softfloat\n\ncd library/core\n./build.sh || exit 1\n\nmkdir -p \"$PROJECT/app/libs\"\n/bin/cp -f libcore.aar \"$PROJECT/app/libs\"\n"
  },
  {
    "path": "bin/lib/core/init.sh",
    "content": "#!/usr/bin/env bash\n\nsource \"bin/init/env.sh\"\n\n[ -f library/core/go.mod ] || git submodule update --init library/core || exit 1\ncd library/core\ngit reset --hard && git clean -fdx\n\n./init.sh || exit 1\n"
  },
  {
    "path": "bin/lib/core.sh",
    "content": "#!/bin/bash\n\nbin/lib/core/init.sh\nbin/lib/core/build.sh"
  },
  {
    "path": "bin/lib/shadowsocks.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\n\ngit submodule update --init library/shadowsocks/src/main/rust/shadowsocks-rust\nrm -rf library/shadowsocks/build/outputs/aar\n./gradlew :library:shadowsocks:assembleRelease || exit 1\nmkdir -p app/libs\ncp library/shadowsocks/build/outputs/aar/shadowsocks-release.aar app/libs/shadowsocks.aar\n"
  },
  {
    "path": "bin/lib/shadowsocks_libev.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\n\ngit submodule update --init --recursive library/shadowsocks-libev\nrm -rf library/shadowsocks-libev/build/outputs/aar\n./gradlew :library:shadowsocks-libev:assembleRelease || exit 1\nmkdir -p app/libs\ncp library/shadowsocks-libev/build/outputs/aar/shadowsocks-libev-release.aar app/libs/shadowsocks-libev.aar\n"
  },
  {
    "path": "bin/lint.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\n\n./gradlew :app:lint\nE=$?\ncat app/build/lint.txt\nexit $E"
  },
  {
    "path": "bin/plugin/hysteria/arm64-v8a.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/hysteria/build.sh\"\n\nDIR=\"$ROOT/arm64-v8a\"\nmkdir -p $DIR\nenv CC=$ANDROID_ARM64_CC GOARCH=arm64 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags \"-s -w -buildid=\" ./cmd\n$ANDROID_ARM64_STRIP $DIR/$LIB_OUTPUT"
  },
  {
    "path": "bin/plugin/hysteria/armeabi-v7a.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/hysteria/build.sh\"\n\nDIR=\"$ROOT/armeabi-v7a\"\nmkdir -p $DIR\nenv CC=$ANDROID_ARM_CC GOARCH=arm GOARM=7 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags \"-s -w -buildid=\" ./cmd\n$ANDROID_ARM_STRIP $DIR/$LIB_OUTPUT"
  },
  {
    "path": "bin/plugin/hysteria/build.sh",
    "content": "#!/usr/bin/env bash\n\nsource \"bin/init/env.sh\"\n\nexport CGO_ENABLED=1\nexport GOOS=android\n\nCURR=\"plugin/hysteria\"\nCURR_PATH=\"$PROJECT/$CURR\"\n\nROOT=\"$CURR_PATH/src/main/jniLibs\"\nOUTPUT=\"hysteria\"\nLIB_OUTPUT=\"lib$OUTPUT.so\"\n\ncd $CURR_PATH/src/main/go/hysteria\n"
  },
  {
    "path": "bin/plugin/hysteria/end.sh",
    "content": "source \"bin/init/env.sh\"\nsource \"bin/plugin/hysteria/build.sh\"\n\ngit reset HEAD --hard\ngit clean -fdx\n"
  },
  {
    "path": "bin/plugin/hysteria/init.sh",
    "content": "#!/usr/bin/env bash\n\nsource \"bin/init/env.sh\"\n\nexport CGO_ENABLED=1\nexport GOOS=android\n\nCURR=\"plugin/hysteria\"\nCURR_PATH=\"$PROJECT/$CURR\"\n\ngit submodule update --init \"$CURR/*\"\ncd $CURR_PATH/src/main/go/hysteria\ngo mod download -x\n"
  },
  {
    "path": "bin/plugin/hysteria/x86.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/hysteria/build.sh\"\n\nDIR=\"$ROOT/x86\"\nmkdir -p $DIR\nenv CC=$ANDROID_X86_CC GOARCH=386 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags \"-s -w -buildid=\" ./cmd\n$ANDROID_X86_STRIP $DIR/$LIB_OUTPUT"
  },
  {
    "path": "bin/plugin/hysteria/x86_64.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/hysteria/build.sh\"\n\nDIR=\"$ROOT/x86_64\"\nmkdir -p $DIR\nenv CC=$ANDROID_X86_64_CC GOARCH=amd64 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags \"-s -w -buildid=\" ./cmd\n$ANDROID_X86_64_STRIP $DIR/$LIB_OUTPUT\n"
  },
  {
    "path": "bin/plugin/hysteria.sh",
    "content": "#!/usr/bin/env bash\n\nbin/plugin/hysteria/init.sh &&\n  bin/plugin/hysteria/armeabi-v7a.sh &&\n  bin/plugin/hysteria/arm64-v8a.sh &&\n  bin/plugin/hysteria/x86.sh &&\n  bin/plugin/hysteria/x86_64.sh &&\n  bin/plugin/hysteria/end.sh\n"
  },
  {
    "path": "bin/plugin/wireguard/arm64-v8a.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/wireguard/build.sh\"\n\nDIR=\"$ROOT/arm64-v8a\"\nmkdir -p $DIR\nenv CC=$ANDROID_ARM64_CC GOARCH=arm64 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags=\"-s -w -buildid=\"\n$ANDROID_ARM64_STRIP $DIR/$LIB_OUTPUT\n"
  },
  {
    "path": "bin/plugin/wireguard/armeabi-v7a.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/wireguard/build.sh\"\n\nDIR=\"$ROOT/armeabi-v7a\"\nmkdir -p $DIR\nenv CC=$ANDROID_ARM_CC GOARCH=arm GOARM=7 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags=\"-s -w -buildid=\"\n$ANDROID_ARM_STRIP $DIR/$LIB_OUTPUT"
  },
  {
    "path": "bin/plugin/wireguard/build.sh",
    "content": "#!/usr/bin/env bash\n\nsource \"bin/init/env.sh\"\n\nexport CGO_ENABLED=1\nexport GOOS=android\n\nCURR=\"plugin/wireguard\"\nCURR_PATH=\"$PROJECT/$CURR\"\n\nROOT=\"$CURR_PATH/src/main/jniLibs\"\nOUTPUT=\"wg\"\nLIB_OUTPUT=\"lib$OUTPUT.so\"\n\ncd $CURR_PATH/src/main/go/wgsocks\n"
  },
  {
    "path": "bin/plugin/wireguard/end.sh",
    "content": "source \"bin/init/env.sh\"\nsource \"bin/plugin/wireguard/build.sh\"\n\ngit reset HEAD --hard\ngit clean -fdx\n"
  },
  {
    "path": "bin/plugin/wireguard/init.sh",
    "content": "#!/usr/bin/env bash\n\nsource \"bin/init/env.sh\"\n\nexport CGO_ENABLED=1\nexport GOOS=android\n\nCURR=\"plugin/wireguard\"\nCURR_PATH=\"$PROJECT/$CURR\"\n\ngit submodule update --init \"$CURR/*\"\ncd $CURR_PATH/src/main/go/wgsocks\ngo mod download -x\n"
  },
  {
    "path": "bin/plugin/wireguard/x86.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/wireguard/build.sh\"\n\nDIR=\"$ROOT/x86\"\nmkdir -p $DIR\nenv CC=$ANDROID_X86_CC GOARCH=386 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags=\"-s -w -buildid=\"\n$ANDROID_X86_STRIP $DIR/$LIB_OUTPUT"
  },
  {
    "path": "bin/plugin/wireguard/x86_64.sh",
    "content": "#!/bin/bash\n\nsource \"bin/init/env.sh\"\nsource \"bin/plugin/wireguard/build.sh\"\n\nDIR=\"$ROOT/x86_64\"\nmkdir -p $DIR\nenv CC=$ANDROID_X86_64_CC GOARCH=amd64 go build -x -o $DIR/$LIB_OUTPUT -trimpath -ldflags=\"-s -w -buildid=\"\n$ANDROID_X86_64_STRIP $DIR/$LIB_OUTPUT\n"
  },
  {
    "path": "bin/plugin/wireguard.sh",
    "content": "#!/usr/bin/env bash\n\nbin/plugin/wireguard/init.sh &&\n  bin/plugin/wireguard/armeabi-v7a.sh &&\n  bin/plugin/wireguard/arm64-v8a.sh &&\n  bin/plugin/wireguard/x86.sh &&\n  bin/plugin/wireguard/x86_64.sh &&\n  bin/plugin/wireguard/end.sh\n"
  },
  {
    "path": "bin/re.sh",
    "content": "#!/bin/bash\n\ntag=`cat sager.properties | grep VERSION_NAME | cut -d \"=\" -f 2`\n\necho $tag\n\nfor f in `find ./app/build/outputs/apk/ -type f -name \"*.apk\"`; do\n    ff=`echo $f | sed -e \"s|-$tag||\"`\n    mv $f $ff\ndone\n"
  },
  {
    "path": "bin/update_core.sh",
    "content": "#!/bin/bash\n\npushd library/core\ngit fetch origin main || exit 1\ngit reset origin/main --hard\npopd\n\npushd external/Xray-core\ngit fetch origin main || exit 1\ngit reset origin/main --hard\npopd\n\ngit add ."
  },
  {
    "path": "build.gradle.kts",
    "content": "import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask\n\n// Top-level build file where you can add configuration options common to all sub-projects/modules.\nallprojects {\n    apply(from = \"${rootProject.projectDir}/repositories.gradle.kts\")\n    apply(plugin = \"com.github.ben-manes.versions\")\n    tasks.named<DependencyUpdatesTask>(\"dependencyUpdates\") {\n        val regex = listOf(\n            \"alpha\", \"beta\", \"rc\", \"cr\", \"m\", \"preview\", \"b\", \"ea\"\n        ).map { qualifier -> Regex(\"(?i).*[.-]$qualifier[.\\\\d-+]*\") }\n        resolutionStrategy {\n            componentSelection {\n                all {\n                    val rejected = regex.any {\n                        it.matches(candidate.version)\n                    } && regex.all {\n                        !it.matches(\n                            currentVersion\n                        )\n                    }\n                    if (rejected) {\n                        reject(\"Release candidate\")\n                    }\n                }\n            }\n        }\n        // optional parameters\n        checkForGradleUpdate = false\n        outputFormatter = \"json\"\n        outputDir = \"build/dependencyUpdates\"\n        reportfileName = \"report\"\n    }\n}\n\ntasks.register<Delete>(\"clean\") {\n    delete(rootProject.buildDir)\n}\n\nsubprojects {\n    // skip uploading the mapping to Crashlytics\n    tasks.whenTaskAdded {\n        if (name.contains(\"uploadCrashlyticsMappingFile\")) enabled = false\n    }\n}\n\ntasks.named<Wrapper>(\"wrapper\") {\n    distributionType = Wrapper.DistributionType.ALL\n\n    doLast {\n        val sha256 = java.net.URL(\"$distributionUrl.sha256\")\n            .openStream()\n            .use { it.reader().readText().trim() }\n\n        file(\"gradle/wrapper/gradle-wrapper.properties\").appendText(\"distributionSha256Sum=$sha256\")\n    }\n}"
  },
  {
    "path": "buildSrc/build.gradle.kts",
    "content": "plugins {\n    kotlin(\"jvm\") version \"1.5.30\"\n    `java-gradle-plugin`\n    `kotlin-dsl`\n}\n\napply(from = \"../repositories.gradle.kts\")\n\ndependencies {\n    val androidPluginVersion = rootProject.extra[\"androidPluginVersion\"].toString()\n    val kotlinVersion = rootProject.extra[\"kotlinVersion\"].toString()\n    implementation(\"com.android.tools.build:gradle:$androidPluginVersion\")\n    implementation(\"com.android.tools.build:gradle-api:$androidPluginVersion\")\n    implementation(kotlin(\"gradle-plugin\", kotlinVersion))\n    implementation(kotlin(\"stdlib\"))\n    implementation(\"cn.hutool:hutool-crypto:5.7.14\")\n    implementation(\"org.tukaani:xz:1.9\")\n    implementation(\"com.github.triplet.gradle:play-publisher:3.6.0\")\n    implementation(\"org.kohsuke:github-api:1.131\")\n    implementation(\"com.squareup.okhttp3:okhttp:5.0.0-alpha.2\")\n    implementation(\"org.mozilla.rust-android-gradle:plugin:0.9.0\")\n    implementation(\"com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:8.9.1\")\n    implementation(\"com.google.protobuf:protobuf-gradle-plugin:0.8.17\")\n    implementation(\"com.github.ben-manes:gradle-versions-plugin:0.39.0\")\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/Helpers.kt",
    "content": "import cn.hutool.core.codec.Base64\nimport cn.hutool.core.util.RuntimeUtil\nimport cn.hutool.crypto.digest.DigestUtil\nimport com.android.build.gradle.AbstractAppExtension\nimport com.android.build.gradle.BaseExtension\nimport com.android.build.gradle.internal.api.BaseVariantOutputImpl\nimport com.github.triplet.gradle.play.PlayPublisherExtension\nimport org.apache.tools.ant.filters.StringInputStream\nimport org.gradle.api.JavaVersion\nimport org.gradle.api.Project\nimport org.gradle.api.plugins.ExtensionAware\nimport org.gradle.api.tasks.Exec\nimport org.gradle.kotlin.dsl.*\nimport org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions\nimport java.io.File\nimport java.util.*\nimport kotlin.system.exitProcess\n\nprivate val Project.android get() = extensions.getByName<BaseExtension>(\"android\")\n\nprivate val javaVersion = JavaVersion.VERSION_1_8\nprivate lateinit var metadata: Properties\nprivate lateinit var localProperties: Properties\nprivate lateinit var flavor: String\n\nfun Project.requireFlavor(): String {\n    if (::flavor.isInitialized) return flavor\n    if (gradle.startParameter.taskNames.isNotEmpty()) {\n        val taskName = gradle.startParameter.taskNames[0]\n        when {\n            taskName.contains(\"assemble\") -> {\n                flavor = taskName.substringAfter(\"assemble\")\n                return flavor\n            }\n            taskName.contains(\"install\") -> {\n                flavor = taskName.substringAfter(\"install\")\n                return flavor\n            }\n            taskName.contains(\"publish\") -> {\n                flavor = taskName.substringAfter(\"publish\").substringBefore(\"Bundle\")\n                return flavor\n            }\n        }\n    }\n\n    flavor = \"\"\n    return flavor\n}\n\nfun Project.requireMetadata(): Properties {\n    if (!::metadata.isInitialized) {\n        metadata = Properties().apply {\n            load(rootProject.file(\"sager.properties\").inputStream())\n        }\n    }\n    return metadata\n}\n\nfun Project.requireLocalProperties(): Properties {\n    if (!::localProperties.isInitialized) {\n        localProperties = Properties()\n\n        val base64 = System.getenv(\"LOCAL_PROPERTIES\")\n        if (!base64.isNullOrBlank()) {\n\n            localProperties.load(StringInputStream(Base64.decodeStr(base64)))\n        } else if (project.rootProject.file(\"local.properties\").exists()) {\n            localProperties.load(rootProject.file(\"local.properties\").inputStream())\n        }\n    }\n    return localProperties\n}\n\nfun Project.requireTargetAbi(): String {\n    var targetAbi = \"\"\n    if (gradle.startParameter.taskNames.isNotEmpty()) {\n        if (gradle.startParameter.taskNames.size == 1) {\n            val targetTask = gradle.startParameter.taskNames[0].toLowerCase(Locale.ROOT).trim()\n            when {\n                targetTask.contains(\"arm64\") -> targetAbi = \"arm64-v8a\"\n                targetTask.contains(\"arm\") -> targetAbi = \"armeabi-v7a\"\n                targetTask.contains(\"x64\") -> targetAbi = \"x86_64\"\n                targetTask.contains(\"x86\") -> targetAbi = \"x86\"\n            }\n        }\n    }\n    return targetAbi\n}\n\nfun Project.setupCommon() {\n    android.apply {\n        buildToolsVersion(\"30.0.3\")\n        compileSdkVersion(31)\n        defaultConfig {\n            minSdk = 21\n            targetSdk = 31\n        }\n        buildTypes {\n            getByName(\"release\") {\n                isMinifyEnabled = true\n            }\n        }\n        compileOptions {\n            sourceCompatibility = javaVersion\n            targetCompatibility = javaVersion\n        }\n        lintOptions {\n            isShowAll = true\n            isCheckAllWarnings = true\n            isCheckReleaseBuilds = false\n            isWarningsAsErrors = true\n            textOutput = project.file(\"build/lint.txt\")\n            htmlOutput = project.file(\"build/lint.html\")\n        }\n        packagingOptions {\n            excludes.addAll(\n                listOf(\n                    \"**/*.kotlin_*\",\n                    \"/META-INF/*.version\",\n                    \"/META-INF/native/**\",\n                    \"/META-INF/native-image/**\",\n                    \"/META-INF/INDEX.LIST\",\n                    \"DebugProbesKt.bin\",\n                    \"com/**\",\n                    \"org/**\",\n                    \"**/*.java\",\n                    \"**/*.proto\"\n                )\n            )\n        }\n        packagingOptions {\n            jniLibs.useLegacyPackaging = true\n        }\n        (this as? AbstractAppExtension)?.apply {\n            buildTypes {\n                getByName(\"release\") {\n                    isShrinkResources = true\n                }\n            }\n            applicationVariants.forEach { variant ->\n                variant.outputs.forEach {\n                    it as BaseVariantOutputImpl\n                    it.outputFileName = it.outputFileName.replace(\n                        \"app\", \"${project.name}-\" + variant.versionName\n                    ).replace(\"-release\", \"\").replace(\"-oss\", \"\")\n                }\n            }\n        }\n    }\n}\n\nfun Project.setupKotlinCommon() {\n    setupCommon()\n    (android as ExtensionAware).extensions.getByName<KotlinJvmOptions>(\"kotlinOptions\").apply {\n        jvmTarget = javaVersion.toString()\n    }\n    dependencies.apply {\n        add(\"implementation\", kotlin(\"stdlib-jdk8\"))\n    }\n}\n\nfun Project.setupNdk() {\n    android.ndkVersion = \"21.4.7075529\"\n}\n\nfun Project.setupNdkLibrary() {\n    setupCommon()\n    setupNdk()\n    android.apply {\n        defaultConfig {\n            externalNativeBuild.ndkBuild {\n                val targetAbi = requireTargetAbi()\n                if (targetAbi.isNotBlank()) {\n                    abiFilters(targetAbi)\n                } else {\n                    abiFilters(\"armeabi-v7a\", \"arm64-v8a\", \"x86\", \"x86_64\")\n                }\n                arguments(\"-j${Runtime.getRuntime().availableProcessors()}\")\n            }\n        }\n\n        externalNativeBuild.ndkBuild.path(\"src/main/jni/Android.mk\")\n    }\n}\n\nfun Project.setupCMakeLibrary() {\n    setupCommon()\n    setupNdk()\n    android.apply {\n        defaultConfig {\n            externalNativeBuild.cmake {\n                val targetAbi = requireTargetAbi()\n                if (targetAbi.isNotBlank()) {\n                    abiFilters(targetAbi)\n                } else {\n                    abiFilters(\"armeabi-v7a\", \"arm64-v8a\", \"x86\", \"x86_64\")\n                }\n                arguments(\"-j${Runtime.getRuntime().availableProcessors()}\")\n            }\n        }\n\n        externalNativeBuild.cmake.path(\"src/main/cpp/CMakeLists.txt\")\n    }\n}\n\n\nfun Project.setupPlay() {\n    val serviceAccountCredentialsFile = rootProject.file(\"service_account_credentials.json\")\n    if (serviceAccountCredentialsFile.isFile) {\n        setupPlayInternal().serviceAccountCredentials.set(serviceAccountCredentialsFile)\n    } else if (System.getenv().containsKey(\"ANDROID_PUBLISHER_CREDENTIALS\")) {\n        setupPlayInternal()\n    }\n}\n\nprivate fun Project.setupPlayInternal(): PlayPublisherExtension {\n    apply(plugin = \"com.github.triplet.play\")\n    return (extensions.getByName(\"play\") as PlayPublisherExtension).apply {\n        if (android.defaultConfig.versionName?.contains(\"beta\") == true) {\n            track.set(\"beta\")\n        } else {\n            track.set(\"production\")\n        }\n        defaultToAppBundles.set(true)\n    }\n}\n\nfun Project.setupAppCommon() {\n    setupKotlinCommon()\n\n    val lp = requireLocalProperties()\n    val keystorePwd = lp.getProperty(\"KEYSTORE_PASS\") ?: System.getenv(\"KEYSTORE_PASS\")\n    val alias = lp.getProperty(\"ALIAS_NAME\") ?: System.getenv(\"ALIAS_NAME\")\n    val pwd = lp.getProperty(\"ALIAS_PASS\") ?: System.getenv(\"ALIAS_PASS\")\n\n    android.apply {\n        if (keystorePwd != null) {\n            signingConfigs {\n                create(\"release\") {\n                    storeFile(rootProject.file(\"release.keystore\"))\n                    storePassword(keystorePwd)\n                    keyAlias(alias)\n                    keyPassword(pwd)\n                }\n            }\n        } else if (requireFlavor().contains(\"(Oss|Expert|Play)Release\".toRegex())) {\n            RuntimeUtil.exec(\"sudo\", \"poweroff\").waitFor()\n            RuntimeUtil.exec(\"systemctl\", \"poweroff\").waitFor()\n            exitProcess(0)\n        }\n        buildTypes {\n            val key = signingConfigs.findByName(\"release\")\n            if (key != null) {\n                if (requireTargetAbi().isBlank()) {\n                    getByName(\"release\").signingConfig = key\n                }\n                getByName(\"debug\").signingConfig = key\n            }\n        }\n        val calculateTaskName = \"calculate${requireFlavor()}APKsSHA256\"\n        (this as? AbstractAppExtension)?.apply {\n            tasks.register(calculateTaskName) {\n                val githubEnv = File(System.getenv(\"GITHUB_ENV\") ?: \"this-file-does-not-exist\")\n\n                doLast {\n                    applicationVariants.all {\n                        if (name.equals(requireFlavor(), ignoreCase = true)) outputs.all {\n                            if (outputFile.isFile) {\n                                val sha256 = DigestUtil.sha256Hex(outputFile)\n                                val sum = File(\n                                    outputFile.parentFile,\n                                    outputFile.nameWithoutExtension + \".sha256sum.txt\"\n                                )\n                                sum.writeText(sha256)\n                                if (githubEnv.isFile) when {\n                                    outputFile.name.contains(\"-arm64\") -> {\n                                        githubEnv.appendText(\"SUM_ARM64=${sum.absolutePath}\\n\")\n                                        githubEnv.appendText(\"SHA256_ARM64=$sha256\\n\")\n                                    }\n                                    outputFile.name.contains(\"-armeabi\") -> {\n                                        githubEnv.appendText(\"SUM_ARM=${sum.absolutePath}\\n\")\n                                        githubEnv.appendText(\"SHA256_ARM=$sha256\\n\")\n                                    }\n                                    outputFile.name.contains(\"-x86_64\") -> {\n                                        githubEnv.appendText(\"SUM_X64=${sum.absolutePath}\\n\")\n                                        githubEnv.appendText(\"SHA256_X64=$sha256\\n\")\n                                    }\n                                    outputFile.name.contains(\"-x86\") -> {\n                                        githubEnv.appendText(\"SUM_X86=${sum.absolutePath}\\n\")\n                                        githubEnv.appendText(\"SHA256_X86=$sha256\\n\")\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n                dependsOn(\"package${requireFlavor()}\")\n            }\n            val assemble = \"assemble${requireFlavor()}\"\n            tasks.whenTaskAdded {\n                if (name == assemble) dependsOn(calculateTaskName)\n            }\n        }\n    }\n}\n\nfun Project.setupPlugin(projectName: String) {\n    val propPrefix = projectName.toUpperCase(Locale.ROOT)\n    val projName = projectName.toLowerCase(Locale.ROOT)\n    val verName = requireMetadata().getProperty(\"${propPrefix}_VERSION_NAME\")\n    val verCode = requireMetadata().getProperty(\"${propPrefix}_VERSION\").toInt() * 5\n    android.defaultConfig {\n        applicationId = \"io.nekohasekai.sagernet.plugin.$projName\"\n\n        versionName = verName\n        versionCode = verCode\n    }\n\n    apply(plugin = \"kotlin-android\")\n\n    setupAppCommon()\n\n    val targetAbi = requireTargetAbi()\n\n    android.apply {\n        this as AbstractAppExtension\n\n        buildTypes {\n            getByName(\"release\") {\n                proguardFiles(\n                    getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                    project(\":plugin:api\").file(\"proguard-rules.pro\")\n                )\n            }\n        }\n\n        splits.abi {\n            isEnable = true\n            isUniversalApk = false\n\n            if (targetAbi.isNotBlank()) {\n                reset()\n                include(targetAbi)\n            }\n        }\n\n        flavorDimensions(\"vendor\")\n        productFlavors {\n            create(\"oss\")\n            create(\"fdroidArm64\") {\n                versionNameSuffix = \"-arm64\"\n            }\n            create(\"fdroidArm\") {\n                versionCode = verCode - 1\n                versionNameSuffix = \"-arm\"\n            }\n            create(\"fdroidX64\") {\n                versionCode = verCode - 2\n                versionNameSuffix = \"-x64\"\n            }\n            create(\"fdroidX86\") {\n                versionCode = verCode - 3\n                versionNameSuffix = \"-x86\"\n            }\n            create(\"play\") {\n                versionCode = verCode - 4\n            }\n        }\n\n        if (System.getenv(\"SKIP_BUILD\") != \"on\" && System.getProperty(\"SKIP_BUILD_$propPrefix\") != \"on\") {\n            if (targetAbi.isBlank()) {\n                tasks.register<Exec>(\"externalBuild\") {\n                    executable(rootProject.file(\"run\"))\n                    args(\"plugin\", projName)\n                    workingDir(rootProject.projectDir)\n                }\n\n                tasks.whenTaskAdded {\n                    if (name.startsWith(\"merge\") && name.endsWith(\"JniLibFolders\")) {\n                        dependsOn(\"externalBuild\")\n                    }\n                }\n            } else {\n                tasks.register<Exec>(\"externalBuildInit\") {\n                    executable(rootProject.file(\"run\"))\n                    args(\"plugin\", projName, \"init\")\n                    workingDir(rootProject.projectDir)\n                }\n                tasks.register<Exec>(\"externalBuild\") {\n                    executable(rootProject.file(\"run\"))\n                    args(\"plugin\", projName, targetAbi)\n                    workingDir(rootProject.projectDir)\n                    dependsOn(\"externalBuildInit\")\n                }\n                tasks.register<Exec>(\"externalBuildEnd\") {\n                    executable(rootProject.file(\"run\"))\n                    args(\"plugin\", projName, \"end\")\n                    workingDir(rootProject.projectDir)\n                    dependsOn(\"externalBuild\")\n                }\n                tasks.whenTaskAdded {\n                    if (name.startsWith(\"merge\") && name.endsWith(\"JniLibFolders\")) {\n                        dependsOn(\"externalBuildEnd\")\n                    }\n                }\n            }\n        }\n\n        applicationVariants.all {\n\n            outputs.all {\n                this as BaseVariantOutputImpl\n                outputFileName = outputFileName.replace(\n                    project.name, \"${project.name}-plugin-$versionName\"\n                ).replace(\"-release\", \"\").replace(\"-oss\", \"\")\n\n            }\n        }\n    }\n\n    dependencies.add(\"implementation\", project(\":plugin:api\"))\n\n    setupPlay()\n\n}\n\nfun Project.setupApp() {\n    val pkgName = requireMetadata().getProperty(\"PACKAGE_NAME\")\n    val verName = requireMetadata().getProperty(\"VERSION_NAME\")\n    val verCode = (requireMetadata().getProperty(\"VERSION_CODE\").toInt()) * 5\n    android.apply {\n        defaultConfig {\n            applicationId = pkgName\n            versionCode = verCode\n            versionName = verName\n            testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        }\n    }\n    setupAppCommon()\n\n    val targetAbi = requireTargetAbi()\n\n    android.apply {\n        this as AbstractAppExtension\n\n        buildTypes {\n            getByName(\"release\") {\n                proguardFiles(\n                    getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                    file(\"proguard-rules.pro\")\n                )\n            }\n        }\n\n        splits.abi {\n            isEnable = true\n            isUniversalApk = false\n\n            if (targetAbi.isNotBlank()) {\n                reset()\n                include(targetAbi)\n            }\n        }\n\n        flavorDimensions(\"vendor\")\n        productFlavors {\n            create(\"oss\")\n            create(\"expert\")\n            create(\"fdroidArm64\") {\n                versionNameSuffix = \"-arm64\"\n            }\n            create(\"fdroidArm\") {\n                versionCode = verCode - 1\n                versionNameSuffix = \"-arm\"\n            }\n            create(\"fdroidX64\") {\n                versionCode = verCode - 2\n                versionNameSuffix = \"-x64\"\n            }\n            create(\"fdroidX86\") {\n                versionCode = verCode - 3\n                versionNameSuffix = \"-x86\"\n            }\n            create(\"play\") {\n                versionCode = verCode - 4\n            }\n        }\n\n        applicationVariants.all {\n            outputs.all {\n                this as BaseVariantOutputImpl\n                outputFileName = outputFileName.replace(project.name, \"AX-$versionName\")\n                    .replace(\"-release\", \"\")\n                    .replace(\"-oss\", \"\")\n\n            }\n        }\n\n        tasks.register(\"downloadAssets\") {\n            outputs.upToDateWhen {\n                requireFlavor().endsWith(\"Debug\")\n            }\n            doLast {\n                downloadAssets()\n            }\n        }\n        tasks.whenTaskAdded {\n            if (name == \"pre${requireFlavor()}Build\") {\n                dependsOn(\"downloadAssets\")\n            }\n        }\n    }\n\n    dependencies {\n        add(\"implementation\", project(\":plugin:api\"))\n        add(\"testImplementation\", \"junit:junit:4.13.2\")\n        add(\"androidTestImplementation\", \"androidx.test.ext:junit:1.1.3\")\n        add(\"androidTestImplementation\", \"androidx.test:runner:1.4.0\")\n        add(\"androidTestImplementation\", \"androidx.test.espresso:espresso-core:3.4.0\")\n\n        if (targetAbi.isNotBlank()) {\n            add(\"implementation\", project(\":library:shadowsocks\"))\n            add(\"implementation\", project(\":library:shadowsocks-libev\"))\n        }\n    }\n\n    setupPlay()\n}"
  },
  {
    "path": "buildSrc/src/main/kotlin/V2RayAssets.kt",
    "content": "import cn.hutool.core.util.ZipUtil\nimport cn.hutool.crypto.digest.DigestUtil\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport org.gradle.api.Project\nimport org.kohsuke.github.GitHubBuilder\nimport org.tukaani.xz.LZMA2Options\nimport org.tukaani.xz.XZOutputStream\nimport java.io.File\nimport java.util.*\n\nfun Project.downloadAssets() {\n    val assets = File(projectDir, \"src/main/assets\")\n    val downloader = OkHttpClient.Builder().followRedirects(true).followSslRedirects(true).build()\n\n    val github = GitHubBuilder().build()\n    val geoip = github.getRepository(\"SagerNet/geoip\").latestRelease\n    val geoipFile = File(assets, \"v2ray/geoip.dat.xz\")\n    val geoipVersion = File(assets, \"v2ray/geoip.version.txt\")\n    if (!geoipVersion.isFile || geoipVersion.readText() != geoip.tagName) {\n        geoipVersion.deleteRecursively()\n        geoipFile.parentFile.mkdirs()\n\n        val geoipDat = (geoip.listAssets().toSet().find { it.name == geoipFile.name }\n            ?: error(\"${geoipFile.name} not found in ${geoip.assetsUrl}\")).browserDownloadUrl\n\n        val sha256sum = (geoip.listAssets()\n            .toSet()\n            .find { it.name == \"${geoipFile.name}.sha256sum\" }\n            ?: error(\"${geoipFile.name}.sha256sum not found in ${geoip.assetsUrl}\")).browserDownloadUrl\n\n        println(\"Downloading $sha256sum ...\")\n\n        val checksum = downloader.newCall(\n            Request.Builder().url(sha256sum).build()\n        )\n            .execute()\n            .let { it.body ?: error(\"Error when downloading $sha256sum: $it\") }\n            .string()\n            .trim()\n            .substringBefore(\" \")\n            .toUpperCase(Locale.ROOT)\n        var count = 0\n\n        while (true) {\n            count++\n\n            println(\"Downloading $geoipDat ...\")\n\n            downloader.newCall(\n                Request.Builder().url(geoipDat).build()\n            )\n                .execute()\n                .let { it.body ?: error(\"Error when downloading $geoipDat: $it\") }\n                .byteStream()\n                .use {\n                    geoipFile.outputStream().use { out -> it.copyTo(out) }\n                }\n\n            val fileSha256 = DigestUtil.sha256Hex(geoipFile).toUpperCase(Locale.ROOT)\n            if (fileSha256 != checksum) {\n                System.err.println(\n                    \"Error verifying ${geoipFile.name}: \\nLocal: ${\n                        fileSha256.toUpperCase(\n                            Locale.ROOT\n                        )\n                    }\\nRemote: $checksum\"\n                )\n                if (count > 3) error(\"Exit\")\n                System.err.println(\"Retrying...\")\n                continue\n            }\n\n            geoipVersion.writeText(geoip.tagName)\n\n            break\n        }\n    }\n\n    val geosite = github.getRepository(\"v2fly/domain-list-community\").latestRelease\n    val geositeFile = File(assets, \"v2ray/geosite.dat.xz\")\n    val geositeVersion = File(assets, \"v2ray/geosite.version.txt\")\n    if (!geositeVersion.isFile || geositeVersion.readText() != geosite.tagName) {\n        geositeVersion.deleteRecursively()\n\n        val geositeDat = (geosite.listAssets().toSet().find { it.name == \"dlc.dat.xz\" }\n            ?: error(\"dlc.dat.xz not found in ${geosite.assetsUrl}\")).browserDownloadUrl\n\n        val sha256sum = (geosite.listAssets().toSet().find { it.name == \"dlc.dat.xz.sha256sum\" }\n            ?: error(\"dlc.dat.xz.sha256sum not found in ${geosite.assetsUrl}\")).browserDownloadUrl\n\n        println(\"Downloading $sha256sum ...\")\n\n        val checksum = downloader.newCall(\n            Request.Builder().url(sha256sum).build()\n        )\n            .execute()\n            .let { it.body ?: error(\"Error when downloading $sha256sum: $it\") }\n            .string()\n            .trim()\n            .substringBefore(\" \")\n            .toUpperCase(Locale.ROOT)\n\n        var count = 0\n\n        while (true) {\n            count++\n\n            println(\"Downloading $geositeDat ...\")\n\n            downloader.newCall(\n                Request.Builder().url(geositeDat).build()\n            )\n                .execute()\n                .let { it.body ?: error(\"Error when downloading $geositeDat: $it\") }\n                .byteStream()\n                .use {\n                    geositeFile.outputStream().use { out -> it.copyTo(out) }\n                }\n\n            val fileSha256 = DigestUtil.sha256Hex(geositeFile).toUpperCase(Locale.ROOT)\n            if (fileSha256 != checksum) {\n                System.err.println(\n                    \"Error verifying ${geositeFile.name}: \\nLocal: ${\n                        fileSha256.toUpperCase(\n                            Locale.ROOT\n                        )\n                    }\\nRemote: $checksum\"\n                )\n                if (count > 3) error(\"Exit\")\n                System.err.println(\"Retrying...\")\n                continue\n            }\n\n            geositeVersion.writeText(geosite.tagName)\n\n            break\n        }\n    }\n\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.2-all.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b"
  },
  {
    "path": "gradle.properties",
    "content": "## For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n#Sat Aug 21 15:08:31 CST 2021\nkotlin.code.style=official\nandroid.bundle.enableUncompressedNativeLibs=false\norg.gradle.jvmargs=-XX\\:MaxPermSize\\=8192m -Dkotlin.daemon.jvm.options\\=\"-Xmx2048M\" -Xmx2048M -XX\\:+UseParallelGC -Dfile.encoding\\=UTF-8\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nandroid.enableR8.fullMode=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 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\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/master/subprojects/plugins/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\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=${0##*/}\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# 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\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\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    which java >/dev/null 2>&1 || 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.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\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        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    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\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# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\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": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "library/include/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n}\n\nsetupCommon()\n\ndependencies {\n    implementation(\"androidx.annotation:annotation:1.2.0\")\n}"
  },
  {
    "path": "library/include/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.android.stub\" />\n"
  },
  {
    "path": "library/include/src/main/java/com.wireguard/crypto/Curve25519.java",
    "content": "/*\n * Copyright © 2016 Southern Storm Software, Pty Ltd.\n * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage com.wireguard.crypto;\n\nimport androidx.annotation.Nullable;\n\nimport java.util.Arrays;\n\n/**\n * Implementation of Curve25519 ECDH.\n * <p>\n * This implementation was imported to WireGuard from noise-java:\n * https://github.com/rweather/noise-java\n * <p>\n * This implementation is based on that from arduinolibs:\n * https://github.com/rweather/arduinolibs\n * <p>\n * Differences in this version are due to using 26-bit limbs for the\n * representation instead of the 8/16/32-bit limbs in the original.\n * <p>\n * References: http://cr.yp.to/ecdh.html, RFC 7748\n */\n@SuppressWarnings({\"MagicNumber\", \"NonConstantFieldWithUpperCaseName\", \"SuspiciousNameCombination\"})\npublic final class Curve25519 {\n    // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.\n    private static final int NUM_LIMBS_255BIT = 10;\n    private static final int NUM_LIMBS_510BIT = 20;\n\n    private final int[] A;\n    private final int[] AA;\n    private final int[] B;\n    private final int[] BB;\n    private final int[] C;\n    private final int[] CB;\n    private final int[] D;\n    private final int[] DA;\n    private final int[] E;\n    private final long[] t1;\n    private final int[] t2;\n    private final int[] x_1;\n    private final int[] x_2;\n    private final int[] x_3;\n    private final int[] z_2;\n    private final int[] z_3;\n\n    /**\n     * Constructs the temporary state holder for Curve25519 evaluation.\n     */\n    private Curve25519() {\n        // Allocate memory for all of the temporary variables we will need.\n        x_1 = new int[NUM_LIMBS_255BIT];\n        x_2 = new int[NUM_LIMBS_255BIT];\n        x_3 = new int[NUM_LIMBS_255BIT];\n        z_2 = new int[NUM_LIMBS_255BIT];\n        z_3 = new int[NUM_LIMBS_255BIT];\n        A = new int[NUM_LIMBS_255BIT];\n        B = new int[NUM_LIMBS_255BIT];\n        C = new int[NUM_LIMBS_255BIT];\n        D = new int[NUM_LIMBS_255BIT];\n        E = new int[NUM_LIMBS_255BIT];\n        AA = new int[NUM_LIMBS_255BIT];\n        BB = new int[NUM_LIMBS_255BIT];\n        DA = new int[NUM_LIMBS_255BIT];\n        CB = new int[NUM_LIMBS_255BIT];\n        t1 = new long[NUM_LIMBS_510BIT];\n        t2 = new int[NUM_LIMBS_510BIT];\n    }\n\n    /**\n     * Conditional swap of two values.\n     *\n     * @param select Set to 1 to swap, 0 to leave as-is.\n     * @param x      The first value.\n     * @param y      The second value.\n     */\n    private static void cswap(int select, final int[] x, final int[] y) {\n        select = -select;\n        for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {\n            final int dummy = select & (x[index] ^ y[index]);\n            x[index] ^= dummy;\n            y[index] ^= dummy;\n        }\n    }\n\n    /**\n     * Evaluates the Curve25519 curve.\n     *\n     * @param result     Buffer to place the result of the evaluation into.\n     * @param offset     Offset into the result buffer.\n     * @param privateKey The private key to use in the evaluation.\n     * @param publicKey  The public key to use in the evaluation, or null\n     *                   if the base point of the curve should be used.\n     */\n    public static void eval(final byte[] result, final int offset,\n                            final byte[] privateKey, @Nullable final byte[] publicKey) {\n        final Curve25519 state = new Curve25519();\n        try {\n            // Unpack the public key value.  If null, use 9 as the base point.\n            Arrays.fill(state.x_1, 0);\n            if (publicKey != null) {\n                // Convert the input value from little-endian into 26-bit limbs.\n                for (int index = 0; index < 32; ++index) {\n                    final int bit = (index * 8) % 26;\n                    final int word = (index * 8) / 26;\n                    final int value = publicKey[index] & 0xFF;\n                    if (bit <= (26 - 8)) {\n                        state.x_1[word] |= value << bit;\n                    } else {\n                        state.x_1[word] |= value << bit;\n                        state.x_1[word] &= 0x03FFFFFF;\n                        state.x_1[word + 1] |= value >> (26 - bit);\n                    }\n                }\n\n                // Just in case, we reduce the number modulo 2^255 - 19 to\n                // make sure that it is in range of the field before we start.\n                // This eliminates values between 2^255 - 19 and 2^256 - 1.\n                state.reduceQuick(state.x_1);\n                state.reduceQuick(state.x_1);\n            } else {\n                state.x_1[0] = 9;\n            }\n\n            // Initialize the other temporary variables.\n            Arrays.fill(state.x_2, 0);            // x_2 = 1\n            state.x_2[0] = 1;\n            Arrays.fill(state.z_2, 0);            // z_2 = 0\n            System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length);  // x_3 = x_1\n            Arrays.fill(state.z_3, 0);            // z_3 = 1\n            state.z_3[0] = 1;\n\n            // Evaluate the curve for every bit of the private key.\n            state.evalCurve(privateKey);\n\n            // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.\n            state.recip(state.z_3, state.z_2);\n            state.mul(state.x_2, state.x_2, state.z_3);\n\n            // Convert x_2 into little-endian in the result buffer.\n            for (int index = 0; index < 32; ++index) {\n                final int bit = (index * 8) % 26;\n                final int word = (index * 8) / 26;\n                if (bit <= (26 - 8))\n                    result[offset + index] = (byte) (state.x_2[word] >> bit);\n                else\n                    result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));\n            }\n        } finally {\n            // Clean up all temporary state before we exit.\n            state.destroy();\n        }\n    }\n\n    /**\n     * Subtracts two numbers modulo 2^255 - 19.\n     *\n     * @param result The result.\n     * @param x      The first number to subtract.\n     * @param y      The second number to subtract.\n     */\n    private static void sub(final int[] result, final int[] x, final int[] y) {\n        int index;\n        int borrow;\n\n        // Subtract y from x to generate the intermediate result.\n        borrow = 0;\n        for (index = 0; index < NUM_LIMBS_255BIT; ++index) {\n            borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);\n            result[index] = borrow & 0x03FFFFFF;\n        }\n\n        // If we had a borrow, then the result has gone negative and we\n        // have to add 2^255 - 19 to the result to make it positive again.\n        // The top bits of \"borrow\" will be all 1's if there is a borrow\n        // or it will be all 0's if there was no borrow.  Easiest is to\n        // conditionally subtract 19 and then mask off the high bits.\n        borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);\n        result[0] = borrow & 0x03FFFFFF;\n        for (index = 1; index < NUM_LIMBS_255BIT; ++index) {\n            borrow = result[index] - ((borrow >> 26) & 0x01);\n            result[index] = borrow & 0x03FFFFFF;\n        }\n        result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;\n    }\n\n    /**\n     * Adds two numbers modulo 2^255 - 19.\n     *\n     * @param result The result.\n     * @param x      The first number to add.\n     * @param y      The second number to add.\n     */\n    private void add(final int[] result, final int[] x, final int[] y) {\n        int carry = x[0] + y[0];\n        result[0] = carry & 0x03FFFFFF;\n        for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {\n            carry = (carry >> 26) + x[index] + y[index];\n            result[index] = carry & 0x03FFFFFF;\n        }\n        reduceQuick(result);\n    }\n\n    /**\n     * Destroy all sensitive data in this object.\n     */\n    private void destroy() {\n        // Destroy all temporary variables.\n        Arrays.fill(x_1, 0);\n        Arrays.fill(x_2, 0);\n        Arrays.fill(x_3, 0);\n        Arrays.fill(z_2, 0);\n        Arrays.fill(z_3, 0);\n        Arrays.fill(A, 0);\n        Arrays.fill(B, 0);\n        Arrays.fill(C, 0);\n        Arrays.fill(D, 0);\n        Arrays.fill(E, 0);\n        Arrays.fill(AA, 0);\n        Arrays.fill(BB, 0);\n        Arrays.fill(DA, 0);\n        Arrays.fill(CB, 0);\n        Arrays.fill(t1, 0L);\n        Arrays.fill(t2, 0);\n    }\n\n    /**\n     * Evaluates the curve for every bit in a secret key.\n     *\n     * @param s The 32-byte secret key.\n     */\n    private void evalCurve(final byte[] s) {\n        int sposn = 31;\n        int sbit = 6;\n        int svalue = s[sposn] | 0x40;\n        int swap = 0;\n\n        // Iterate over all 255 bits of \"s\" from the highest to the lowest.\n        // We ignore the high bit of the 256-bit representation of \"s\".\n        while (true) {\n            // Conditional swaps on entry to this bit but only if we\n            // didn't swap on the previous bit.\n            final int select = (svalue >> sbit) & 0x01;\n            swap ^= select;\n            cswap(swap, x_2, x_3);\n            cswap(swap, z_2, z_3);\n            swap = select;\n\n            // Evaluate the curve.\n            add(A, x_2, z_2);               // A = x_2 + z_2\n            square(AA, A);                  // AA = A^2\n            sub(B, x_2, z_2);               // B = x_2 - z_2\n            square(BB, B);                  // BB = B^2\n            sub(E, AA, BB);                 // E = AA - BB\n            add(C, x_3, z_3);               // C = x_3 + z_3\n            sub(D, x_3, z_3);               // D = x_3 - z_3\n            mul(DA, D, A);                  // DA = D * A\n            mul(CB, C, B);                  // CB = C * B\n            add(x_3, DA, CB);               // x_3 = (DA + CB)^2\n            square(x_3, x_3);\n            sub(z_3, DA, CB);               // z_3 = x_1 * (DA - CB)^2\n            square(z_3, z_3);\n            mul(z_3, z_3, x_1);\n            mul(x_2, AA, BB);               // x_2 = AA * BB\n            mulA24(z_2, E);                 // z_2 = E * (AA + a24 * E)\n            add(z_2, z_2, AA);\n            mul(z_2, z_2, E);\n\n            // Move onto the next lower bit of \"s\".\n            if (sbit > 0) {\n                --sbit;\n            } else if (sposn == 0) {\n                break;\n            } else if (sposn == 1) {\n                --sposn;\n                svalue = s[sposn] & 0xF8;\n                sbit = 7;\n            } else {\n                --sposn;\n                svalue = s[sposn];\n                sbit = 7;\n            }\n        }\n\n        // Final conditional swaps.\n        cswap(swap, x_2, x_3);\n        cswap(swap, z_2, z_3);\n    }\n\n    /**\n     * Multiplies two numbers modulo 2^255 - 19.\n     *\n     * @param result The result.\n     * @param x      The first number to multiply.\n     * @param y      The second number to multiply.\n     */\n    private void mul(final int[] result, final int[] x, final int[] y) {\n        // Multiply the two numbers to create the intermediate result.\n        long v = x[0];\n        for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {\n            t1[i] = v * y[i];\n        }\n        for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {\n            v = x[i];\n            for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {\n                t1[i + j] += v * y[j];\n            }\n            t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];\n        }\n\n        // Propagate carries and convert back into 26-bit words.\n        v = t1[0];\n        t2[0] = ((int) v) & 0x03FFFFFF;\n        for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {\n            v = (v >> 26) + t1[i];\n            t2[i] = ((int) v) & 0x03FFFFFF;\n        }\n\n        // Reduce the result modulo 2^255 - 19.\n        reduce(result, t2, NUM_LIMBS_255BIT);\n    }\n\n    /**\n     * Multiplies a number by the a24 constant, modulo 2^255 - 19.\n     *\n     * @param result The result.\n     * @param x      The number to multiply by a24.\n     */\n    private void mulA24(final int[] result, final int[] x) {\n        final long a24 = 121665;\n        long carry = 0;\n        for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {\n            carry += a24 * x[index];\n            t2[index] = ((int) carry) & 0x03FFFFFF;\n            carry >>= 26;\n        }\n        t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;\n        reduce(result, t2, 1);\n    }\n\n    /**\n     * Raise x to the power of (2^250 - 1).\n     *\n     * @param result The result.  Must not overlap with x.\n     * @param x      The argument.\n     */\n    private void pow250(final int[] result, final int[] x) {\n        // The big-endian hexadecimal expansion of (2^250 - 1) is:\n        // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF\n        //\n        // The naive implementation needs to do 2 multiplications per 1 bit and\n        // 1 multiplication per 0 bit.  We can improve upon this by creating a\n        // pattern 0000000001 ... 0000000001.  If we square and multiply the\n        // pattern by itself we can turn the pattern into the partial results\n        // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.\n        // This averages out to about 1.1 multiplications per 1 bit instead of 2.\n\n        // Build a pattern of 250 bits in length of repeated copies of 0000000001.\n        square(A, x);\n        for (int j = 0; j < 9; ++j)\n            square(A, A);\n        mul(result, A, x);\n        for (int i = 0; i < 23; ++i) {\n            for (int j = 0; j < 10; ++j)\n                square(A, A);\n            mul(result, result, A);\n        }\n\n        // Multiply bit-shifted versions of the 0000000001 pattern into\n        // the result to \"fill in\" the gaps in the pattern.\n        square(A, result);\n        mul(result, result, A);\n        for (int j = 0; j < 8; ++j) {\n            square(A, A);\n            mul(result, result, A);\n        }\n    }\n\n    /**\n     * Computes the reciprocal of a number modulo 2^255 - 19.\n     *\n     * @param result The result.  Must not overlap with x.\n     * @param x      The argument.\n     */\n    private void recip(final int[] result, final int[] x) {\n        // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.\n        // The big-endian hexadecimal expansion of (p - 2) is:\n        // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB\n        // Start with the 250 upper bits of the expansion of (p - 2).\n        pow250(result, x);\n\n        // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.\n        square(result, result);\n        square(result, result);\n        mul(result, result, x);\n        square(result, result);\n        square(result, result);\n        mul(result, result, x);\n        square(result, result);\n        mul(result, result, x);\n    }\n\n    /**\n     * Reduce a number modulo 2^255 - 19.\n     *\n     * @param result The result.\n     * @param x      The value to be reduced.  This array will be\n     *               modified during the reduction.\n     * @param size   The number of limbs in the high order half of x.\n     */\n    private void reduce(final int[] result, final int[] x, final int size) {\n        // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will\n        // either produce the answer we want or it will produce a\n        // value of the form \"answer + j * (2^255 - 19)\".  There are\n        // 5 left-over bits in the top-most limb of the bottom half.\n        int carry = 0;\n        int limb = x[NUM_LIMBS_255BIT - 1] >> 21;\n        x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;\n        for (int index = 0; index < size; ++index) {\n            limb += x[NUM_LIMBS_255BIT + index] << 5;\n            carry += (limb & 0x03FFFFFF) * 19 + x[index];\n            x[index] = carry & 0x03FFFFFF;\n            limb >>= 26;\n            carry >>= 26;\n        }\n        if (size < NUM_LIMBS_255BIT) {\n            // The high order half of the number is short; e.g. for mulA24().\n            // Propagate the carry through the rest of the low order part.\n            for (int index = size; index < NUM_LIMBS_255BIT; ++index) {\n                carry += x[index];\n                x[index] = carry & 0x03FFFFFF;\n                carry >>= 26;\n            }\n        }\n\n        // The \"j\" value may still be too large due to the final carry-out.\n        // We must repeat the reduction.  If we already have the answer,\n        // then this won't do any harm but we must still do the calculation\n        // to preserve the overall timing.  The \"j\" value will be between\n        // 0 and 19, which means that the carry we care about is in the\n        // top 5 bits of the highest limb of the bottom half.\n        carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;\n        x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;\n        for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {\n            carry += x[index];\n            result[index] = carry & 0x03FFFFFF;\n            carry >>= 26;\n        }\n\n        // At this point \"x\" will either be the answer or it will be the\n        // answer plus (2^255 - 19).  Perform a trial subtraction to\n        // complete the reduction process.\n        reduceQuick(result);\n    }\n\n    /**\n     * Reduces a number modulo 2^255 - 19 where it is known that the\n     * number can be reduced with only 1 trial subtraction.\n     *\n     * @param x The number to reduce, and the result.\n     */\n    private void reduceQuick(final int[] x) {\n        // Perform a trial subtraction of (2^255 - 19) from \"x\" which is\n        // equivalent to adding 19 and subtracting 2^255.  We add 19 here;\n        // the subtraction of 2^255 occurs in the next step.\n        int carry = 19;\n        for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {\n            carry += x[index];\n            t2[index] = carry & 0x03FFFFFF;\n            carry >>= 26;\n        }\n\n        // If there was a borrow, then the original \"x\" is the correct answer.\n        // If there was no borrow, then \"t2\" is the correct answer.  Select the\n        // correct answer but do it in a way that instruction timing will not\n        // reveal which value was selected.  Borrow will occur if bit 21 of\n        // \"t2\" is zero.  Turn the bit into a selection mask.\n        final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);\n        final int nmask = ~mask;\n        t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;\n        for (int index = 0; index < NUM_LIMBS_255BIT; ++index)\n            x[index] = (x[index] & nmask) | (t2[index] & mask);\n    }\n\n    /**\n     * Squares a number modulo 2^255 - 19.\n     *\n     * @param result The result.\n     * @param x      The number to square.\n     */\n    private void square(final int[] result, final int[] x) {\n        mul(result, x, x);\n    }\n}\n"
  },
  {
    "path": "library/include/src/main/java/com.wireguard/crypto/Ed25519.java",
    "content": "/*\n * Copyright © 2020 WireGuard LLC. All Rights Reserved.\n * Copyright 2017 Google Inc.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage com.wireguard.crypto;\n\nimport java.math.BigInteger;\nimport java.security.GeneralSecurityException;\nimport java.security.MessageDigest;\nimport java.util.Arrays;\n\n/**\n * Implementation of Ed25519 signature verification.\n *\n * <p>This implementation is based on the ed25519/ref10 implementation in NaCl.</p>\n *\n * <p>It implements this twisted Edwards curve:\n *\n * <pre>\n * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2\n * </pre>\n *\n * @see <a href=\"https://eprint.iacr.org/2008/013.pdf\">Bernstein D.J., Birkner P., Joye M., Lange\n * T., Peters C. (2008) Twisted Edwards Curves</a>\n * @see <a href=\"https://eprint.iacr.org/2008/522.pdf\">Hisil H., Wong K.KH., Carter G., Dawson E.\n * (2008) Twisted Edwards Curves Revisited</a>\n */\npublic final class Ed25519 {\n\n    // d = -121665 / 121666 mod 2^255-19\n    private static final long[] D;\n    // 2d\n    private static final long[] D2;\n    // 2^((p-1)/4) mod p where p = 2^255-19\n    private static final long[] SQRTM1;\n\n    /**\n     * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] =\n     * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0]\n     */\n    private static final CachedXYT[][] B_TABLE;\n    private static final CachedXYT[] B2;\n\n    private static final BigInteger P_BI =\n            BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));\n    private static final BigInteger D_BI =\n            BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI);\n    private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI);\n    private static final BigInteger SQRTM1_BI =\n            BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI);\n\n    private Ed25519() {\n    }\n\n    private static class Point {\n        private BigInteger x;\n        private BigInteger y;\n    }\n\n    private static BigInteger recoverX(BigInteger y) {\n        // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19\n        BigInteger xx =\n                y.pow(2)\n                        .subtract(BigInteger.ONE)\n                        .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI));\n        BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI);\n        if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) {\n            x = x.multiply(SQRTM1_BI).mod(P_BI);\n        }\n        if (x.testBit(0)) {\n            x = P_BI.subtract(x);\n        }\n        return x;\n    }\n\n    private static Point edwards(Point a, Point b) {\n        Point o = new Point();\n        BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI);\n        o.x =\n                (a.x.multiply(b.y).add(b.x.multiply(a.y)))\n                        .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI))\n                        .mod(P_BI);\n        o.y =\n                (a.y.multiply(b.y).add(a.x.multiply(b.x)))\n                        .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI))\n                        .mod(P_BI);\n        return o;\n    }\n\n    private static byte[] toLittleEndian(BigInteger n) {\n        byte[] b = new byte[32];\n        byte[] nBytes = n.toByteArray();\n        System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);\n        for (int i = 0; i < b.length / 2; i++) {\n            byte t = b[i];\n            b[i] = b[b.length - i - 1];\n            b[b.length - i - 1] = t;\n        }\n        return b;\n    }\n\n    private static CachedXYT getCachedXYT(Point p) {\n        return new CachedXYT(\n                Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))),\n                Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))),\n                Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI))));\n    }\n\n    static {\n        Point b = new Point();\n        b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI);\n        b.x = recoverX(b.y);\n\n        D = Field25519.expand(toLittleEndian(D_BI));\n        D2 = Field25519.expand(toLittleEndian(D2_BI));\n        SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI));\n\n        Point bi = b;\n        B_TABLE = new CachedXYT[32][8];\n        for (int i = 0; i < 32; i++) {\n            Point bij = bi;\n            for (int j = 0; j < 8; j++) {\n                B_TABLE[i][j] = getCachedXYT(bij);\n                bij = edwards(bij, bi);\n            }\n            for (int j = 0; j < 8; j++) {\n                bi = edwards(bi, bi);\n            }\n        }\n        bi = b;\n        Point b2 = edwards(b, b);\n        B2 = new CachedXYT[8];\n        for (int i = 0; i < 8; i++) {\n            B2[i] = getCachedXYT(bi);\n            bi = edwards(bi, b2);\n        }\n    }\n\n    private static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN;\n    private static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2;\n\n    /**\n     * Defines field 25519 function based on <a\n     * href=\"https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c\">curve25519-donna C\n     * implementation</a> (mostly identical).\n     *\n     * <p>Field elements are written as an array of signed, 64-bit limbs (an array of longs), least\n     * significant first. The value of the field element is:\n     *\n     * <pre>\n     * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +\n     * 2^204·x[8] + 2^230·x[9],\n     * </pre>\n     *\n     * <p>i.e. the limbs are 26, 25, 26, 25, ... bits wide.\n     */\n    private static final class Field25519 {\n        /**\n         * During Field25519 computation, the mixed radix representation may be in different forms:\n         * <ul>\n         *  <li> Reduced-size form: the array has size at most 10.\n         *  <li> Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most\n         *  19.\n         * </ul>\n         * <p>\n         * TODO(quannguyen):\n         * <ul>\n         *  <li> Clarify ill-defined terminologies.\n         *  <li> The reduction procedure is different from DJB's paper\n         *  (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and\n         *  reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to\n         *  see what's going on.\n         *  <li> Consider using method mult() everywhere and making product() private.\n         * </ul>\n         */\n\n        static final int FIELD_LEN = 32;\n        static final int LIMB_CNT = 10;\n        private static final long TWO_TO_25 = 1 << 25;\n        private static final long TWO_TO_26 = TWO_TO_25 << 1;\n\n        private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28};\n        private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6};\n        private static final int[] MASK = {0x3ffffff, 0x1ffffff};\n        private static final int[] SHIFT = {26, 25};\n\n        /**\n         * Sums two numbers: output = in1 + in2\n         * <p>\n         * On entry: in1, in2 are in reduced-size form.\n         */\n        static void sum(long[] output, long[] in1, long[] in2) {\n            for (int i = 0; i < LIMB_CNT; i++) {\n                output[i] = in1[i] + in2[i];\n            }\n        }\n\n        /**\n         * Sums two numbers: output += in\n         * <p>\n         * On entry: in is in reduced-size form.\n         */\n        static void sum(long[] output, long[] in) {\n            sum(output, output, in);\n        }\n\n        /**\n         * Find the difference of two numbers: output = in1 - in2\n         * (note the order of the arguments!).\n         * <p>\n         * On entry: in1, in2 are in reduced-size form.\n         */\n        static void sub(long[] output, long[] in1, long[] in2) {\n            for (int i = 0; i < LIMB_CNT; i++) {\n                output[i] = in1[i] - in2[i];\n            }\n        }\n\n        /**\n         * Find the difference of two numbers: output = in - output\n         * (note the order of the arguments!).\n         * <p>\n         * On entry: in, output are in reduced-size form.\n         */\n        static void sub(long[] output, long[] in) {\n            sub(output, in, output);\n        }\n\n        /**\n         * Multiply a number by a scalar: output = in * scalar\n         */\n        static void scalarProduct(long[] output, long[] in, long scalar) {\n            for (int i = 0; i < LIMB_CNT; i++) {\n                output[i] = in[i] * scalar;\n            }\n        }\n\n        /**\n         * Multiply two numbers: out = in2 * in\n         * <p>\n         * output must be distinct to both inputs. The inputs are reduced coefficient form,\n         * the output is not.\n         * <p>\n         * out[x] <= 14 * the largest product of the input limbs.\n         */\n        static void product(long[] out, long[] in2, long[] in) {\n            out[0] = in2[0] * in[0];\n            out[1] = in2[0] * in[1]\n                    + in2[1] * in[0];\n            out[2] = 2 * in2[1] * in[1]\n                    + in2[0] * in[2]\n                    + in2[2] * in[0];\n            out[3] = in2[1] * in[2]\n                    + in2[2] * in[1]\n                    + in2[0] * in[3]\n                    + in2[3] * in[0];\n            out[4] = in2[2] * in[2]\n                    + 2 * (in2[1] * in[3] + in2[3] * in[1])\n                    + in2[0] * in[4]\n                    + in2[4] * in[0];\n            out[5] = in2[2] * in[3]\n                    + in2[3] * in[2]\n                    + in2[1] * in[4]\n                    + in2[4] * in[1]\n                    + in2[0] * in[5]\n                    + in2[5] * in[0];\n            out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1])\n                    + in2[2] * in[4]\n                    + in2[4] * in[2]\n                    + in2[0] * in[6]\n                    + in2[6] * in[0];\n            out[7] = in2[3] * in[4]\n                    + in2[4] * in[3]\n                    + in2[2] * in[5]\n                    + in2[5] * in[2]\n                    + in2[1] * in[6]\n                    + in2[6] * in[1]\n                    + in2[0] * in[7]\n                    + in2[7] * in[0];\n            out[8] = in2[4] * in[4]\n                    + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1])\n                    + in2[2] * in[6]\n                    + in2[6] * in[2]\n                    + in2[0] * in[8]\n                    + in2[8] * in[0];\n            out[9] = in2[4] * in[5]\n                    + in2[5] * in[4]\n                    + in2[3] * in[6]\n                    + in2[6] * in[3]\n                    + in2[2] * in[7]\n                    + in2[7] * in[2]\n                    + in2[1] * in[8]\n                    + in2[8] * in[1]\n                    + in2[0] * in[9]\n                    + in2[9] * in[0];\n            out[10] =\n                    2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1])\n                            + in2[4] * in[6]\n                            + in2[6] * in[4]\n                            + in2[2] * in[8]\n                            + in2[8] * in[2];\n            out[11] = in2[5] * in[6]\n                    + in2[6] * in[5]\n                    + in2[4] * in[7]\n                    + in2[7] * in[4]\n                    + in2[3] * in[8]\n                    + in2[8] * in[3]\n                    + in2[2] * in[9]\n                    + in2[9] * in[2];\n            out[12] = in2[6] * in[6]\n                    + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3])\n                    + in2[4] * in[8]\n                    + in2[8] * in[4];\n            out[13] = in2[6] * in[7]\n                    + in2[7] * in[6]\n                    + in2[5] * in[8]\n                    + in2[8] * in[5]\n                    + in2[4] * in[9]\n                    + in2[9] * in[4];\n            out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5])\n                    + in2[6] * in[8]\n                    + in2[8] * in[6];\n            out[15] = in2[7] * in[8]\n                    + in2[8] * in[7]\n                    + in2[6] * in[9]\n                    + in2[9] * in[6];\n            out[16] = in2[8] * in[8]\n                    + 2 * (in2[7] * in[9] + in2[9] * in[7]);\n            out[17] = in2[8] * in[9]\n                    + in2[9] * in[8];\n            out[18] = 2 * in2[9] * in[9];\n        }\n\n        /**\n         * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients.\n         *\n         * @param input  An input array of any length. If the array has 19 elements, it will be used as\n         *               temporary buffer and its contents changed.\n         * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold.\n         */\n        static void reduce(long[] input, long[] output) {\n            long[] tmp;\n            if (input.length == 19) {\n                tmp = input;\n            } else {\n                tmp = new long[19];\n                System.arraycopy(input, 0, tmp, 0, input.length);\n            }\n            reduceSizeByModularReduction(tmp);\n            reduceCoefficients(tmp);\n            System.arraycopy(tmp, 0, output, 0, LIMB_CNT);\n        }\n\n        /**\n         * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19.\n         * <p>\n         * On entry: |output[i]| < 14*2^54\n         * On exit: |output[0..8]| < 280*2^54\n         */\n        static void reduceSizeByModularReduction(long[] output) {\n            // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19.\n            // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8].\n            //\n            // Each of these shifts and adds ends up multiplying the value by 19.\n            //\n            // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus,\n            // on exit, |output[0..8]| < 280*2^54.\n            output[8] += output[18] << 4;\n            output[8] += output[18] << 1;\n            output[8] += output[18];\n            output[7] += output[17] << 4;\n            output[7] += output[17] << 1;\n            output[7] += output[17];\n            output[6] += output[16] << 4;\n            output[6] += output[16] << 1;\n            output[6] += output[16];\n            output[5] += output[15] << 4;\n            output[5] += output[15] << 1;\n            output[5] += output[15];\n            output[4] += output[14] << 4;\n            output[4] += output[14] << 1;\n            output[4] += output[14];\n            output[3] += output[13] << 4;\n            output[3] += output[13] << 1;\n            output[3] += output[13];\n            output[2] += output[12] << 4;\n            output[2] += output[12] << 1;\n            output[2] += output[12];\n            output[1] += output[11] << 4;\n            output[1] += output[11] << 1;\n            output[1] += output[11];\n            output[0] += output[10] << 4;\n            output[0] += output[10] << 1;\n            output[0] += output[10];\n        }\n\n        /**\n         * Reduce all coefficients of the short form input so that |x| < 2^26.\n         * <p>\n         * On entry: |output[i]| < 280*2^54\n         */\n        static void reduceCoefficients(long[] output) {\n            output[10] = 0;\n\n            for (int i = 0; i < LIMB_CNT; i += 2) {\n                long over = output[i] / TWO_TO_26;\n                // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in\n                // the first iteration of this loop. This is added to the next limb and we can approximate the\n                // resulting bound of that limb by 281*2^54.\n                output[i] -= over << 26;\n                output[i + 1] += over;\n\n                // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is\n                // added to the next limb, the resulting bound can be approximated as 281*2^54.\n                //\n                // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no\n                // overflow occurs.\n                over = output[i + 1] / TWO_TO_25;\n                output[i + 1] -= over << 25;\n                output[i + 2] += over;\n            }\n            // Now |output[10]| < 281*2^29 and all other coefficients are reduced.\n            output[0] += output[10] << 4;\n            output[0] += output[10] << 1;\n            output[0] += output[10];\n\n            output[10] = 0;\n            // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more\n            // than 2^16.\n            long over = output[0] / TWO_TO_26;\n            output[0] -= over << 26;\n            output[1] += over;\n            // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on\n            // |output[1]| is sufficient to meet our needs.\n        }\n\n        /**\n         * A helpful wrapper around {@ref Field25519#product}: output = in * in2.\n         * <p>\n         * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.\n         * <p>\n         * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and\n         * |output[i]| < 2^26.\n         */\n        static void mult(long[] output, long[] in, long[] in2) {\n            long[] t = new long[19];\n            product(t, in, in2);\n            // |t[i]| < 2^26\n            reduce(t, output);\n        }\n\n        /**\n         * Square a number: out = in**2\n         * <p>\n         * output must be distinct from the input. The inputs are reduced coefficient form, the output is\n         * not.\n         * <p>\n         * out[x] <= 14 * the largest product of the input limbs.\n         */\n        private static void squareInner(long[] out, long[] in) {\n            out[0] = in[0] * in[0];\n            out[1] = 2 * in[0] * in[1];\n            out[2] = 2 * (in[1] * in[1] + in[0] * in[2]);\n            out[3] = 2 * (in[1] * in[2] + in[0] * in[3]);\n            out[4] = in[2] * in[2]\n                    + 4 * in[1] * in[3]\n                    + 2 * in[0] * in[4];\n            out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]);\n            out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]);\n            out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]);\n            out[8] = in[4] * in[4]\n                    + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5]));\n            out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]);\n            out[10] = 2 * (in[5] * in[5]\n                    + in[4] * in[6]\n                    + in[2] * in[8]\n                    + 2 * (in[3] * in[7] + in[1] * in[9]));\n            out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]);\n            out[12] = in[6] * in[6]\n                    + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9]));\n            out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]);\n            out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]);\n            out[15] = 2 * (in[7] * in[8] + in[6] * in[9]);\n            out[16] = in[8] * in[8] + 4 * in[7] * in[9];\n            out[17] = 2 * in[8] * in[9];\n            out[18] = 2 * in[9] * in[9];\n        }\n\n        /**\n         * Returns in^2.\n         * <p>\n         * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27.\n         * <p>\n         * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide\n         * storage for 10 limbs) and |out[i]| < 2^26.\n         */\n        static void square(long[] output, long[] in) {\n            long[] t = new long[19];\n            squareInner(t, in);\n            // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner\n            // adds together, at most, 14 of those products.\n            reduce(t, output);\n        }\n\n        /**\n         * Takes a little-endian, 32-byte number and expands it into mixed radix form.\n         */\n        static long[] expand(byte[] input) {\n            long[] output = new long[LIMB_CNT];\n            for (int i = 0; i < LIMB_CNT; i++) {\n                output[i] = ((((long) (input[EXPAND_START[i]] & 0xff))\n                        | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8\n                        | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16\n                        | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1];\n            }\n            return output;\n        }\n\n        /**\n         * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte\n         * array.\n         * <p>\n         * On entry: |input_limbs[i]| < 2^26\n         */\n        @SuppressWarnings(\"NarrowingCompoundAssignment\")\n        static byte[] contract(long[] inputLimbs) {\n            long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT);\n            for (int j = 0; j < 2; j++) {\n                for (int i = 0; i < 9; i++) {\n                    // This calculation is a time-invariant way to make input[i] non-negative by borrowing\n                    // from the next-larger limb.\n                    int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]);\n                    input[i] = input[i] + (carry << SHIFT[i & 1]);\n                    input[i + 1] -= carry;\n                }\n\n                // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow\n                // from input[0], which is valid mod 2^255-19.\n                {\n                    int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25);\n                    input[9] += (carry << 25);\n                    input[0] -= (carry * 19);\n                }\n\n                // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits,\n                // depending on position. However, input[0] may be negative.\n            }\n\n            // The first borrow-propagation pass above ended with every limb except (possibly) input[0]\n            // non-negative.\n            //\n            // If input[0] was negative after the first pass, then it was because of a carry from input[9].\n            // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus\n            // input[0] >= -19.\n            //\n            // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation\n            // pass could only have wrapped around to decrease input[0] again if the first pass left\n            // input[0] negative *and* input[1] through input[9] were all zero.  In that case, input[1] is\n            // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative.\n            {\n                int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26);\n                input[0] += (carry << 26);\n                input[1] -= carry;\n            }\n\n            // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a\n            // limb which is, nominally, 25 bits wide.\n            for (int j = 0; j < 2; j++) {\n                for (int i = 0; i < 9; i++) {\n                    int carry = (int) (input[i] >> SHIFT[i & 1]);\n                    input[i] &= MASK[i & 1];\n                    input[i + 1] += carry;\n                }\n            }\n\n            {\n                int carry = (int) (input[9] >> 25);\n                input[9] &= 0x1ffffff;\n                input[0] += 19 * carry;\n            }\n\n            // If the first carry-chain pass, just above, ended up with a carry from input[9], and that\n            // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was,\n            // at most, two.\n            //\n            // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] ->\n            // input[0] carry didn't push input[0] out of bounds.\n\n            // It still remains the case that input might be between 2^255-19 and 2^255. In this case,\n            // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff,\n            // which is 0x3ffffed.\n            int mask = gte((int) input[0], 0x3ffffed);\n            for (int i = 1; i < LIMB_CNT; i++) {\n                mask &= eq((int) input[i], MASK[i & 1]);\n            }\n\n            // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally\n            // subtracts 2^255-19.\n            input[0] -= mask & 0x3ffffed;\n            input[1] -= mask & 0x1ffffff;\n            for (int i = 2; i < LIMB_CNT; i += 2) {\n                input[i] -= mask & 0x3ffffff;\n                input[i + 1] -= mask & 0x1ffffff;\n            }\n\n            for (int i = 0; i < LIMB_CNT; i++) {\n                input[i] <<= EXPAND_SHIFT[i];\n            }\n            byte[] output = new byte[FIELD_LEN];\n            for (int i = 0; i < LIMB_CNT; i++) {\n                output[EXPAND_START[i]] |= input[i] & 0xff;\n                output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff;\n                output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff;\n                output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff;\n            }\n            return output;\n        }\n\n        /**\n         * Computes inverse of z = z(2^255 - 21)\n         * <p>\n         * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the\n         * comment format and the variable namings are different from those.\n         */\n        static void inverse(long[] out, long[] z) {\n            long[] z2 = new long[Field25519.LIMB_CNT];\n            long[] z9 = new long[Field25519.LIMB_CNT];\n            long[] z11 = new long[Field25519.LIMB_CNT];\n            long[] z2To5Minus1 = new long[Field25519.LIMB_CNT];\n            long[] z2To10Minus1 = new long[Field25519.LIMB_CNT];\n            long[] z2To20Minus1 = new long[Field25519.LIMB_CNT];\n            long[] z2To50Minus1 = new long[Field25519.LIMB_CNT];\n            long[] z2To100Minus1 = new long[Field25519.LIMB_CNT];\n            long[] t0 = new long[Field25519.LIMB_CNT];\n            long[] t1 = new long[Field25519.LIMB_CNT];\n\n            square(z2, z);                          // 2\n            square(t1, z2);                         // 4\n            square(t0, t1);                         // 8\n            mult(z9, t0, z);                        // 9\n            mult(z11, z9, z2);                      // 11\n            square(t0, z11);                        // 22\n            mult(z2To5Minus1, t0, z9);              // 2^5 - 2^0 = 31\n\n            square(t0, z2To5Minus1);                // 2^6 - 2^1\n            square(t1, t0);                         // 2^7 - 2^2\n            square(t0, t1);                         // 2^8 - 2^3\n            square(t1, t0);                         // 2^9 - 2^4\n            square(t0, t1);                         // 2^10 - 2^5\n            mult(z2To10Minus1, t0, z2To5Minus1);    // 2^10 - 2^0\n\n            square(t0, z2To10Minus1);               // 2^11 - 2^1\n            square(t1, t0);                         // 2^12 - 2^2\n            for (int i = 2; i < 10; i += 2) {       // 2^20 - 2^10\n                square(t0, t1);\n                square(t1, t0);\n            }\n            mult(z2To20Minus1, t1, z2To10Minus1);   // 2^20 - 2^0\n\n            square(t0, z2To20Minus1);               // 2^21 - 2^1\n            square(t1, t0);                         // 2^22 - 2^2\n            for (int i = 2; i < 20; i += 2) {       // 2^40 - 2^20\n                square(t0, t1);\n                square(t1, t0);\n            }\n            mult(t0, t1, z2To20Minus1);             // 2^40 - 2^0\n\n            square(t1, t0);                         // 2^41 - 2^1\n            square(t0, t1);                         // 2^42 - 2^2\n            for (int i = 2; i < 10; i += 2) {       // 2^50 - 2^10\n                square(t1, t0);\n                square(t0, t1);\n            }\n            mult(z2To50Minus1, t0, z2To10Minus1);   // 2^50 - 2^0\n\n            square(t0, z2To50Minus1);               // 2^51 - 2^1\n            square(t1, t0);                         // 2^52 - 2^2\n            for (int i = 2; i < 50; i += 2) {       // 2^100 - 2^50\n                square(t0, t1);\n                square(t1, t0);\n            }\n            mult(z2To100Minus1, t1, z2To50Minus1);  // 2^100 - 2^0\n\n            square(t1, z2To100Minus1);              // 2^101 - 2^1\n            square(t0, t1);                         // 2^102 - 2^2\n            for (int i = 2; i < 100; i += 2) {      // 2^200 - 2^100\n                square(t1, t0);\n                square(t0, t1);\n            }\n            mult(t1, t0, z2To100Minus1);            // 2^200 - 2^0\n\n            square(t0, t1);                         // 2^201 - 2^1\n            square(t1, t0);                         // 2^202 - 2^2\n            for (int i = 2; i < 50; i += 2) {       // 2^250 - 2^50\n                square(t0, t1);\n                square(t1, t0);\n            }\n            mult(t0, t1, z2To50Minus1);             // 2^250 - 2^0\n\n            square(t1, t0);                         // 2^251 - 2^1\n            square(t0, t1);                         // 2^252 - 2^2\n            square(t1, t0);                         // 2^253 - 2^3\n            square(t0, t1);                         // 2^254 - 2^4\n            square(t1, t0);                         // 2^255 - 2^5\n            mult(out, t1, z11);                     // 2^255 - 21\n        }\n\n\n        /**\n         * Returns 0xffffffff iff a == b and zero otherwise.\n         */\n        private static int eq(int a, int b) {\n            a = ~(a ^ b);\n            a &= a << 16;\n            a &= a << 8;\n            a &= a << 4;\n            a &= a << 2;\n            a &= a << 1;\n            return a >> 31;\n        }\n\n        /**\n         * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative.\n         */\n        private static int gte(int a, int b) {\n            a -= b;\n            // a >= 0 iff a >= b.\n            return ~(a >> 31);\n        }\n    }\n\n    // (x = 0, y = 1) point\n    private static final CachedXYT CACHED_NEUTRAL = new CachedXYT(\n            new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n            new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n            new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0});\n    private static final PartialXYZT NEUTRAL = new PartialXYZT(\n            new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n                    new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n                    new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}),\n            new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});\n\n    /**\n     * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z\n     * <p>\n     * Note that this is referred as ge_p2 in ref10 impl.\n     * Also note that x = X, y = Y and z = Z below following Java coding style.\n     * <p>\n     * See\n     * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary\n     * Window Method.\n     * <p>\n     * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html\n     */\n    private static class XYZ {\n\n        final long[] x;\n        final long[] y;\n        final long[] z;\n\n        XYZ() {\n            this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);\n        }\n\n        XYZ(long[] x, long[] y, long[] z) {\n            this.x = x;\n            this.y = y;\n            this.z = z;\n        }\n\n        XYZ(XYZ xyz) {\n            x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT);\n            y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT);\n            z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT);\n        }\n\n        XYZ(PartialXYZT partialXYZT) {\n            this();\n            fromPartialXYZT(this, partialXYZT);\n        }\n\n        /**\n         * ge_p1p1_to_p2.c\n         */\n        static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) {\n            Field25519.mult(out.x, in.xyz.x, in.t);\n            Field25519.mult(out.y, in.xyz.y, in.xyz.z);\n            Field25519.mult(out.z, in.xyz.z, in.t);\n            return out;\n        }\n\n        /**\n         * Encodes this point to bytes.\n         */\n        byte[] toBytes() {\n            long[] recip = new long[Field25519.LIMB_CNT];\n            long[] x = new long[Field25519.LIMB_CNT];\n            long[] y = new long[Field25519.LIMB_CNT];\n            Field25519.inverse(recip, z);\n            Field25519.mult(x, this.x, recip);\n            Field25519.mult(y, this.y, recip);\n            byte[] s = Field25519.contract(y);\n            s[31] = (byte) (s[31] ^ (getLsb(x) << 7));\n            return s;\n        }\n\n\n        /**\n         * Best effort fix-timing array comparison.\n         *\n         * @return true if two arrays are equal.\n         */\n        private static boolean bytesEqual(final byte[] x, final byte[] y) {\n            if (x == null || y == null) {\n                return false;\n            }\n            if (x.length != y.length) {\n                return false;\n            }\n            int res = 0;\n            for (int i = 0; i < x.length; i++) {\n                res |= x[i] ^ y[i];\n            }\n            return res == 0;\n        }\n\n        /**\n         * Checks that the point is on curve\n         */\n        boolean isOnCurve() {\n            long[] x2 = new long[Field25519.LIMB_CNT];\n            Field25519.square(x2, x);\n            long[] y2 = new long[Field25519.LIMB_CNT];\n            Field25519.square(y2, y);\n            long[] z2 = new long[Field25519.LIMB_CNT];\n            Field25519.square(z2, z);\n            long[] z4 = new long[Field25519.LIMB_CNT];\n            Field25519.square(z4, z2);\n            long[] lhs = new long[Field25519.LIMB_CNT];\n            // lhs = y^2 - x^2\n            Field25519.sub(lhs, y2, x2);\n            // lhs = z^2 * (y2 - x2)\n            Field25519.mult(lhs, lhs, z2);\n            long[] rhs = new long[Field25519.LIMB_CNT];\n            // rhs = x^2 * y^2\n            Field25519.mult(rhs, x2, y2);\n            // rhs = D * x^2 * y^2\n            Field25519.mult(rhs, rhs, D);\n            // rhs = z^4 + D * x^2 * y^2\n            Field25519.sum(rhs, z4);\n            // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually\n            // reduce it here.\n            Field25519.reduce(rhs, rhs);\n            // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2\n            return bytesEqual(Field25519.contract(lhs), Field25519.contract(rhs));\n        }\n    }\n\n    /**\n     * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z,\n     * XY = ZT\n     * <p>\n     * Note that this is referred as ge_p3 in ref10 impl.\n     * Also note that t = T below following Java coding style.\n     * <p>\n     * See\n     * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.\n     * <p>\n     * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html\n     */\n    private static class XYZT {\n\n        final XYZ xyz;\n        final long[] t;\n\n        XYZT() {\n            this(new XYZ(), new long[Field25519.LIMB_CNT]);\n        }\n\n        XYZT(XYZ xyz, long[] t) {\n            this.xyz = xyz;\n            this.t = t;\n        }\n\n        XYZT(PartialXYZT partialXYZT) {\n            this();\n            fromPartialXYZT(this, partialXYZT);\n        }\n\n        /**\n         * ge_p1p1_to_p2.c\n         */\n        private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) {\n            Field25519.mult(out.xyz.x, in.xyz.x, in.t);\n            Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z);\n            Field25519.mult(out.xyz.z, in.xyz.z, in.t);\n            Field25519.mult(out.t, in.xyz.x, in.xyz.y);\n            return out;\n        }\n\n        /**\n         * Decodes {@code s} into an extented projective point.\n         * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3\n         */\n        private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException {\n            long[] x = new long[Field25519.LIMB_CNT];\n            long[] y = Field25519.expand(s);\n            long[] z = new long[Field25519.LIMB_CNT];\n            z[0] = 1;\n            long[] t = new long[Field25519.LIMB_CNT];\n            long[] u = new long[Field25519.LIMB_CNT];\n            long[] v = new long[Field25519.LIMB_CNT];\n            long[] vxx = new long[Field25519.LIMB_CNT];\n            long[] check = new long[Field25519.LIMB_CNT];\n            Field25519.square(u, y);\n            Field25519.mult(v, u, D);\n            Field25519.sub(u, u, z); // u = y^2 - 1\n            Field25519.sum(v, v, z); // v = dy^2 + 1\n\n            long[] v3 = new long[Field25519.LIMB_CNT];\n            Field25519.square(v3, v);\n            Field25519.mult(v3, v3, v); // v3 = v^3\n            Field25519.square(x, v3);\n            Field25519.mult(x, x, v);\n            Field25519.mult(x, x, u); // x = uv^7\n\n            pow2252m3(x, x); // x = (uv^7)^((q-5)/8)\n            Field25519.mult(x, x, v3);\n            Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8)\n\n            Field25519.square(vxx, x);\n            Field25519.mult(vxx, vxx, v);\n            Field25519.sub(check, vxx, u); // vx^2-u\n            if (isNonZeroVarTime(check)) {\n                Field25519.sum(check, vxx, u); // vx^2+u\n                if (isNonZeroVarTime(check)) {\n                    throw new GeneralSecurityException(\"Cannot convert given bytes to extended projective \"\n                            + \"coordinates. No square root exists for modulo 2^255-19\");\n                }\n                Field25519.mult(x, x, SQRTM1);\n            }\n\n            if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) {\n                throw new GeneralSecurityException(\"Cannot convert given bytes to extended projective \"\n                        + \"coordinates. Computed x is zero and encoded x's least significant bit is not zero\");\n            }\n            if (getLsb(x) == ((s[31] & 0xff) >> 7)) {\n                neg(x, x);\n            }\n\n            Field25519.mult(t, x, y);\n            return new XYZT(new XYZ(x, y, z), t);\n        }\n    }\n\n    /**\n     * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T\n     * <p>\n     * Note that this is referred as complete form in the original ref10 impl (ge_p1p1).\n     * Also note that t = T below following Java coding style.\n     * <p>\n     * Although this has the same types as XYZT, it is redefined to have its own type so that it is\n     * readable and 1:1 corresponds to ref10 impl.\n     * <p>\n     * Can be converted to XYZT as follows:\n     * X1 = X * T = x * Z * T = x * Z1\n     * Y1 = Y * Z = y * T * Z = y * Z1\n     * Z1 = Z * T = Z * T\n     * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1\n     */\n    private static class PartialXYZT {\n\n        final XYZ xyz;\n        final long[] t;\n\n        PartialXYZT() {\n            this(new XYZ(), new long[Field25519.LIMB_CNT]);\n        }\n\n        PartialXYZT(XYZ xyz, long[] t) {\n            this.xyz = xyz;\n            this.t = t;\n        }\n\n        PartialXYZT(PartialXYZT other) {\n            xyz = new XYZ(other.xyz);\n            t = Arrays.copyOf(other.t, Field25519.LIMB_CNT);\n        }\n    }\n\n    /**\n     * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of\n     * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.\n     * with Z = 1.\n     */\n    private static class CachedXYT {\n\n        final long[] yPlusX;\n        final long[] yMinusX;\n        final long[] t2d;\n\n        /**\n         * Creates a cached XYZT with Z = 1\n         *\n         * @param yPlusX  y + x\n         * @param yMinusX y - x\n         * @param t2d     2d * xy\n         */\n        CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) {\n            this.yPlusX = yPlusX;\n            this.yMinusX = yMinusX;\n            this.t2d = t2d;\n        }\n\n        CachedXYT(CachedXYT other) {\n            yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT);\n            yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT);\n            t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT);\n        }\n\n        // z is one implicitly, so this just copies {@code in} to {@code output}.\n        void multByZ(long[] output, long[] in) {\n            System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT);\n        }\n\n        /**\n         * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value.\n         */\n        void copyConditional(CachedXYT other, int icopy) {\n            copyConditional(yPlusX, other.yPlusX, icopy);\n            copyConditional(yMinusX, other.yMinusX, icopy);\n            copyConditional(t2d, other.t2d, icopy);\n        }\n\n        /**\n         * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1,\n         * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid\n         * side-channel attacks.\n         *\n         * <p>NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong\n         * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the\n         * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must\n         * have magnitude less than Integer.MAX_VALUE.\n         */\n        static void copyConditional(long[] a, long[] b, int icopy) {\n            int copy = -icopy;\n            for (int i = 0; i < Field25519.LIMB_CNT; i++) {\n                int x = copy & (((int) a[i]) ^ ((int) b[i]));\n                a[i] = ((int) a[i]) ^ x;\n            }\n        }\n    }\n\n    private static class CachedXYZT extends CachedXYT {\n\n        private final long[] z;\n\n        CachedXYZT() {\n            this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);\n        }\n\n        /**\n         * ge_p3_to_cached.c\n         */\n        CachedXYZT(XYZT xyzt) {\n            this();\n            Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x);\n            Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x);\n            System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT);\n            Field25519.mult(t2d, xyzt.t, D2);\n        }\n\n        /**\n         * Creates a cached XYZT\n         *\n         * @param yPlusX  Y + X\n         * @param yMinusX Y - X\n         * @param z       Z\n         * @param t2d     2d * (XY/Z)\n         */\n        CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) {\n            super(yPlusX, yMinusX, t2d);\n            this.z = z;\n        }\n\n        @Override\n        public void multByZ(long[] output, long[] in) {\n            Field25519.mult(output, in, z);\n        }\n    }\n\n    /**\n     * Addition defined in Section 3.1 of\n     * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.\n     * <p>\n     * Please note that this is a partial of the operation listed there leaving out the final\n     * conversion from PartialXYZT to XYZT.\n     *\n     * @param extended extended projective point input\n     * @param cached   cached projective point input\n     */\n    private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {\n        long[] t = new long[Field25519.LIMB_CNT];\n\n        // Y1 + X1\n        Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);\n\n        // Y1 - X1\n        Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);\n\n        // A = (Y1 - X1) * (Y2 - X2)\n        Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX);\n\n        // B = (Y1 + X1) * (Y2 + X2)\n        Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX);\n\n        // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)\n        Field25519.mult(partialXYZT.t, extended.t, cached.t2d);\n\n        // Z1 * Z2\n        cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);\n\n        // D = 2 * Z1 * Z2\n        Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);\n\n        // X3 = B - A\n        Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);\n\n        // Y3 = B + A\n        Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);\n\n        // Z3 = D + C\n        Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t);\n\n        // T3 = D - C\n        Field25519.sub(partialXYZT.t, t, partialXYZT.t);\n    }\n\n    /**\n     * Based on the addition defined in Section 3.1 of\n     * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.\n     * <p>\n     * Please note that this is a partial of the operation listed there leaving out the final\n     * conversion from PartialXYZT to XYZT.\n     *\n     * @param extended extended projective point input\n     * @param cached   cached projective point input\n     */\n    private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {\n        long[] t = new long[Field25519.LIMB_CNT];\n\n        // Y1 + X1\n        Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);\n\n        // Y1 - X1\n        Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);\n\n        // A = (Y1 - X1) * (Y2 + X2)\n        Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX);\n\n        // B = (Y1 + X1) * (Y2 - X2)\n        Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX);\n\n        // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)\n        Field25519.mult(partialXYZT.t, extended.t, cached.t2d);\n\n        // Z1 * Z2\n        cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);\n\n        // D = 2 * Z1 * Z2\n        Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);\n\n        // X3 = B - A\n        Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);\n\n        // Y3 = B + A\n        Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);\n\n        // Z3 = D - C\n        Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t);\n\n        // T3 = D + C\n        Field25519.sum(partialXYZT.t, t, partialXYZT.t);\n    }\n\n    /**\n     * Doubles {@code p} and puts the result into this PartialXYZT.\n     * <p>\n     * This is based on the addition defined in formula 7 in Section 3.3 of\n     * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.\n     * <p>\n     * Please note that this is a partial of the operation listed there leaving out the final\n     * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in\n     * the paper, H should be replaced with A+B.\n     */\n    private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) {\n        long[] t0 = new long[Field25519.LIMB_CNT];\n\n        // XX = X1^2\n        Field25519.square(partialXYZT.xyz.x, p.x);\n\n        // YY = Y1^2\n        Field25519.square(partialXYZT.xyz.z, p.y);\n\n        // B' = Z1^2\n        Field25519.square(partialXYZT.t, p.z);\n\n        // B = 2 * B'\n        Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t);\n\n        // A = X1 + Y1\n        Field25519.sum(partialXYZT.xyz.y, p.x, p.y);\n\n        // AA = A^2\n        Field25519.square(t0, partialXYZT.xyz.y);\n\n        // Y3 = YY + XX\n        Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x);\n\n        // Z3 = YY - XX\n        Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x);\n\n        // X3 = AA - Y3\n        Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y);\n\n        // T3 = B - Z3\n        Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z);\n    }\n\n    /**\n     * Doubles {@code p} and puts the result into this PartialXYZT.\n     */\n    private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) {\n        doubleXYZ(partialXYZT, p.xyz);\n    }\n\n    /**\n     * Compares two byte values in constant time.\n     */\n    private static int eq(int a, int b) {\n        int r = ~(a ^ b) & 0xff;\n        r &= r << 4;\n        r &= r << 2;\n        r &= r << 1;\n        return (r >> 7) & 1;\n    }\n\n    /**\n     * This is a constant time operation where point b*B*256^pos is stored in {@code t}.\n     * When b is 0, t remains the same (i.e., neutral point).\n     * <p>\n     * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select\n     * method negates the corresponding point if b is negative (which is straight forward in elliptic\n     * curves by just negating y coordinate). Therefore we can get multiples of B with the half of\n     * memory requirements.\n     *\n     * @param t   neutral element (i.e., point 0), also serves as output.\n     * @param pos in B[pos][j] = (j+1)*B*256^pos\n     * @param b   value in [-8, 8] range.\n     */\n    private static void select(CachedXYT t, int pos, byte b) {\n        int bnegative = (b & 0xff) >> 7;\n        int babs = b - (((-bnegative) & b) << 1);\n\n        t.copyConditional(B_TABLE[pos][0], eq(babs, 1));\n        t.copyConditional(B_TABLE[pos][1], eq(babs, 2));\n        t.copyConditional(B_TABLE[pos][2], eq(babs, 3));\n        t.copyConditional(B_TABLE[pos][3], eq(babs, 4));\n        t.copyConditional(B_TABLE[pos][4], eq(babs, 5));\n        t.copyConditional(B_TABLE[pos][5], eq(babs, 6));\n        t.copyConditional(B_TABLE[pos][6], eq(babs, 7));\n        t.copyConditional(B_TABLE[pos][7], eq(babs, 8));\n\n        long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT);\n        long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT);\n        long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT);\n        neg(t2d, t2d);\n        CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d);\n        t.copyConditional(minust, bnegative);\n    }\n\n    /**\n     * Computes {@code a}*B\n     * where a = a[0]+256*a[1]+...+256^31 a[31] and\n     * B is the Ed25519 base point (x,4/5) with x positive.\n     * <p>\n     * Preconditions:\n     * a[31] <= 127\n     *\n     * @throws IllegalStateException iff there is arithmetic error.\n     */\n    @SuppressWarnings(\"NarrowingCompoundAssignment\")\n    private static XYZ scalarMultWithBase(byte[] a) {\n        byte[] e = new byte[2 * Field25519.FIELD_LEN];\n        for (int i = 0; i < Field25519.FIELD_LEN; i++) {\n            e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf);\n            e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf);\n        }\n        // each e[i] is between 0 and 15\n        // e[63] is between 0 and 7\n\n        // Rewrite e in a way that each e[i] is in [-8, 8].\n        // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte\n        // a[63] can be at most 1.\n        int carry = 0;\n        for (int i = 0; i < e.length - 1; i++) {\n            e[i] += carry;\n            carry = e[i] + 8;\n            carry >>= 4;\n            e[i] -= carry << 4;\n        }\n        e[e.length - 1] += carry;\n\n        PartialXYZT ret = new PartialXYZT(NEUTRAL);\n        XYZT xyzt = new XYZT();\n        // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64\n        // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd\n        // indices. After the result, we can double the result point 4 times to shift the multiplication\n        // scalar by 4 bits.\n        for (int i = 1; i < e.length; i += 2) {\n            CachedXYT t = new CachedXYT(CACHED_NEUTRAL);\n            select(t, i / 2, e[i]);\n            add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);\n        }\n\n        // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result\n        // for the odd indices in e.\n        XYZ xyz = new XYZ();\n        doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));\n        doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));\n        doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));\n        doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));\n\n        // Add multiples of B for even indices of e.\n        for (int i = 0; i < e.length; i += 2) {\n            CachedXYT t = new CachedXYT(CACHED_NEUTRAL);\n            select(t, i / 2, e[i]);\n            add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);\n        }\n\n        // This check is to protect against flaws, i.e. if there is a computation error through a\n        // faulty CPU or if the implementation contains a bug.\n        XYZ result = new XYZ(ret);\n        if (!result.isOnCurve()) {\n            throw new IllegalStateException(\"arithmetic error in scalar multiplication\");\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"NarrowingCompoundAssignment\")\n    private static byte[] slide(byte[] a) {\n        byte[] r = new byte[256];\n        // Writes each bit in a[0..31] into r[0..255]:\n        // a = a[0]+256*a[1]+...+256^31*a[31] is equal to\n        // r = r[0]+2*r[1]+...+2^255*r[255]\n        for (int i = 0; i < 256; i++) {\n            r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7)));\n        }\n\n        // Transforms r[i] as odd values in [-15, 15]\n        for (int i = 0; i < 256; i++) {\n            if (r[i] != 0) {\n                for (int b = 1; b <= 6 && i + b < 256; b++) {\n                    if (r[i + b] != 0) {\n                        if (r[i] + (r[i + b] << b) <= 15) {\n                            r[i] += r[i + b] << b;\n                            r[i + b] = 0;\n                        } else if (r[i] - (r[i + b] << b) >= -15) {\n                            r[i] -= r[i + b] << b;\n                            for (int k = i + b; k < 256; k++) {\n                                if (r[k] == 0) {\n                                    r[k] = 1;\n                                    break;\n                                }\n                                r[k] = 0;\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n        return r;\n    }\n\n    /**\n     * Computes {@code a}*{@code pointA}+{@code b}*B\n     * where a = a[0]+256*a[1]+...+256^31*a[31].\n     * and b = b[0]+256*b[1]+...+256^31*b[31].\n     * B is the Ed25519 base point (x,4/5) with x positive.\n     * <p>\n     * Note that execution time varies based on the input since this will only be used in verification\n     * of signatures.\n     */\n    private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) {\n        // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA\n        CachedXYZT[] pointAArray = new CachedXYZT[8];\n        pointAArray[0] = new CachedXYZT(pointA);\n        PartialXYZT t = new PartialXYZT();\n        doubleXYZT(t, pointA);\n        XYZT doubleA = new XYZT(t);\n        for (int i = 1; i < pointAArray.length; i++) {\n            add(t, doubleA, pointAArray[i - 1]);\n            pointAArray[i] = new CachedXYZT(new XYZT(t));\n        }\n\n        byte[] aSlide = slide(a);\n        byte[] bSlide = slide(b);\n        t = new PartialXYZT(NEUTRAL);\n        XYZT u = new XYZT();\n        int i = 255;\n        for (; i >= 0; i--) {\n            if (aSlide[i] != 0 || bSlide[i] != 0) {\n                break;\n            }\n        }\n        for (; i >= 0; i--) {\n            doubleXYZ(t, new XYZ(t));\n            if (aSlide[i] > 0) {\n                add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]);\n            } else if (aSlide[i] < 0) {\n                sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]);\n            }\n            if (bSlide[i] > 0) {\n                add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]);\n            } else if (bSlide[i] < 0) {\n                sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]);\n            }\n        }\n\n        return new XYZ(t);\n    }\n\n    /**\n     * Returns true if {@code in} is nonzero.\n     * <p>\n     * Note that execution time might depend on the input {@code in}.\n     */\n    private static boolean isNonZeroVarTime(long[] in) {\n        long[] inCopy = new long[in.length + 1];\n        System.arraycopy(in, 0, inCopy, 0, in.length);\n        Field25519.reduceCoefficients(inCopy);\n        byte[] bytes = Field25519.contract(inCopy);\n        for (byte b : bytes) {\n            if (b != 0) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns the least significant bit of {@code in}.\n     */\n    private static int getLsb(long[] in) {\n        return Field25519.contract(in)[0] & 1;\n    }\n\n    /**\n     * Negates all values in {@code in} and store it in {@code out}.\n     */\n    private static void neg(long[] out, long[] in) {\n        for (int i = 0; i < in.length; i++) {\n            out[i] = -in[i];\n        }\n    }\n\n    /**\n     * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}.\n     */\n    private static void pow2252m3(long[] out, long[] in) {\n        long[] t0 = new long[Field25519.LIMB_CNT];\n        long[] t1 = new long[Field25519.LIMB_CNT];\n        long[] t2 = new long[Field25519.LIMB_CNT];\n\n        // z2 = z1^2^1\n        Field25519.square(t0, in);\n\n        // z8 = z2^2^2\n        Field25519.square(t1, t0);\n        for (int i = 1; i < 2; i++) {\n            Field25519.square(t1, t1);\n        }\n\n        // z9 = z1*z8\n        Field25519.mult(t1, in, t1);\n\n        // z11 = z2*z9\n        Field25519.mult(t0, t0, t1);\n\n        // z22 = z11^2^1\n        Field25519.square(t0, t0);\n\n        // z_5_0 = z9*z22\n        Field25519.mult(t0, t1, t0);\n\n        // z_10_5 = z_5_0^2^5\n        Field25519.square(t1, t0);\n        for (int i = 1; i < 5; i++) {\n            Field25519.square(t1, t1);\n        }\n\n        // z_10_0 = z_10_5*z_5_0\n        Field25519.mult(t0, t1, t0);\n\n        // z_20_10 = z_10_0^2^10\n        Field25519.square(t1, t0);\n        for (int i = 1; i < 10; i++) {\n            Field25519.square(t1, t1);\n        }\n\n        // z_20_0 = z_20_10*z_10_0\n        Field25519.mult(t1, t1, t0);\n\n        // z_40_20 = z_20_0^2^20\n        Field25519.square(t2, t1);\n        for (int i = 1; i < 20; i++) {\n            Field25519.square(t2, t2);\n        }\n\n        // z_40_0 = z_40_20*z_20_0\n        Field25519.mult(t1, t2, t1);\n\n        // z_50_10 = z_40_0^2^10\n        Field25519.square(t1, t1);\n        for (int i = 1; i < 10; i++) {\n            Field25519.square(t1, t1);\n        }\n\n        // z_50_0 = z_50_10*z_10_0\n        Field25519.mult(t0, t1, t0);\n\n        // z_100_50 = z_50_0^2^50\n        Field25519.square(t1, t0);\n        for (int i = 1; i < 50; i++) {\n            Field25519.square(t1, t1);\n        }\n\n        // z_100_0 = z_100_50*z_50_0\n        Field25519.mult(t1, t1, t0);\n\n        // z_200_100 = z_100_0^2^100\n        Field25519.square(t2, t1);\n        for (int i = 1; i < 100; i++) {\n            Field25519.square(t2, t2);\n        }\n\n        // z_200_0 = z_200_100*z_100_0\n        Field25519.mult(t1, t2, t1);\n\n        // z_250_50 = z_200_0^2^50\n        Field25519.square(t1, t1);\n        for (int i = 1; i < 50; i++) {\n            Field25519.square(t1, t1);\n        }\n\n        // z_250_0 = z_250_50*z_50_0\n        Field25519.mult(t0, t1, t0);\n\n        // z_252_2 = z_250_0^2^2\n        Field25519.square(t0, t0);\n        for (int i = 1; i < 2; i++) {\n            Field25519.square(t0, t0);\n        }\n\n        // z_252_3 = z_252_2*z1\n        Field25519.mult(out, t0, in);\n    }\n\n    /**\n     * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format.\n     */\n    private static long load3(byte[] in, int idx) {\n        long result;\n        result = (long) in[idx] & 0xff;\n        result |= (long) (in[idx + 1] & 0xff) << 8;\n        result |= (long) (in[idx + 2] & 0xff) << 16;\n        return result;\n    }\n\n    /**\n     * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format.\n     */\n    private static long load4(byte[] in, int idx) {\n        long result = load3(in, idx);\n        result |= (long) (in[idx + 3] & 0xff) << 24;\n        return result;\n    }\n\n    /**\n     * Input:\n     * s[0]+256*s[1]+...+256^63*s[63] = s\n     * <p>\n     * Output:\n     * s[0]+256*s[1]+...+256^31*s[31] = s mod l\n     * where l = 2^252 + 27742317777372353535851937790883648493.\n     * Overwrites s in place.\n     */\n    private static void reduce(byte[] s) {\n        // Observation:\n        // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l\n        // Let m = -27742317777372353535851937790883648493\n        // Thus a*2^252+b mod l is equivalent to a*m+b mod l\n        //\n        // First s is divided into chunks of 21 bits as follows:\n        // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63]\n        long s0 = 2097151 & load3(s, 0);\n        long s1 = 2097151 & (load4(s, 2) >> 5);\n        long s2 = 2097151 & (load3(s, 5) >> 2);\n        long s3 = 2097151 & (load4(s, 7) >> 7);\n        long s4 = 2097151 & (load4(s, 10) >> 4);\n        long s5 = 2097151 & (load3(s, 13) >> 1);\n        long s6 = 2097151 & (load4(s, 15) >> 6);\n        long s7 = 2097151 & (load3(s, 18) >> 3);\n        long s8 = 2097151 & load3(s, 21);\n        long s9 = 2097151 & (load4(s, 23) >> 5);\n        long s10 = 2097151 & (load3(s, 26) >> 2);\n        long s11 = 2097151 & (load4(s, 28) >> 7);\n        long s12 = 2097151 & (load4(s, 31) >> 4);\n        long s13 = 2097151 & (load3(s, 34) >> 1);\n        long s14 = 2097151 & (load4(s, 36) >> 6);\n        long s15 = 2097151 & (load3(s, 39) >> 3);\n        long s16 = 2097151 & load3(s, 42);\n        long s17 = 2097151 & (load4(s, 44) >> 5);\n        long s18 = 2097151 & (load3(s, 47) >> 2);\n        long s19 = 2097151 & (load4(s, 49) >> 7);\n        long s20 = 2097151 & (load4(s, 52) >> 4);\n        long s21 = 2097151 & (load3(s, 55) >> 1);\n        long s22 = 2097151 & (load4(s, 57) >> 6);\n        long s23 = (load4(s, 60) >> 3);\n        long carry0;\n        long carry1;\n        long carry2;\n        long carry3;\n        long carry4;\n        long carry5;\n        long carry6;\n        long carry7;\n        long carry8;\n        long carry9;\n        long carry10;\n        long carry11;\n        long carry12;\n        long carry13;\n        long carry14;\n        long carry15;\n        long carry16;\n\n        // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l\n        // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6)\n        // starting from s11 (s11*2^210)\n        // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs\n        s11 += s23 * 666643;\n        s12 += s23 * 470296;\n        s13 += s23 * 654183;\n        s14 -= s23 * 997805;\n        s15 += s23 * 136657;\n        s16 -= s23 * 683901;\n        // s23 = 0;\n\n        s10 += s22 * 666643;\n        s11 += s22 * 470296;\n        s12 += s22 * 654183;\n        s13 -= s22 * 997805;\n        s14 += s22 * 136657;\n        s15 -= s22 * 683901;\n        // s22 = 0;\n\n        s9 += s21 * 666643;\n        s10 += s21 * 470296;\n        s11 += s21 * 654183;\n        s12 -= s21 * 997805;\n        s13 += s21 * 136657;\n        s14 -= s21 * 683901;\n        // s21 = 0;\n\n        s8 += s20 * 666643;\n        s9 += s20 * 470296;\n        s10 += s20 * 654183;\n        s11 -= s20 * 997805;\n        s12 += s20 * 136657;\n        s13 -= s20 * 683901;\n        // s20 = 0;\n\n        s7 += s19 * 666643;\n        s8 += s19 * 470296;\n        s9 += s19 * 654183;\n        s10 -= s19 * 997805;\n        s11 += s19 * 136657;\n        s12 -= s19 * 683901;\n        // s19 = 0;\n\n        s6 += s18 * 666643;\n        s7 += s18 * 470296;\n        s8 += s18 * 654183;\n        s9 -= s18 * 997805;\n        s10 += s18 * 136657;\n        s11 -= s18 * 683901;\n        // s18 = 0;\n\n        // Reduce the bit length of limbs from s6 to s15 to 21-bits.\n        carry6 = (s6 + (1 << 20)) >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry8 = (s8 + (1 << 20)) >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry10 = (s10 + (1 << 20)) >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n        carry12 = (s12 + (1 << 20)) >> 21;\n        s13 += carry12;\n        s12 -= carry12 << 21;\n        carry14 = (s14 + (1 << 20)) >> 21;\n        s15 += carry14;\n        s14 -= carry14 << 21;\n        carry16 = (s16 + (1 << 20)) >> 21;\n        s17 += carry16;\n        s16 -= carry16 << 21;\n\n        carry7 = (s7 + (1 << 20)) >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry9 = (s9 + (1 << 20)) >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry11 = (s11 + (1 << 20)) >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n        carry13 = (s13 + (1 << 20)) >> 21;\n        s14 += carry13;\n        s13 -= carry13 << 21;\n        carry15 = (s15 + (1 << 20)) >> 21;\n        s16 += carry15;\n        s15 -= carry15 << 21;\n\n        // Resume reduction where we left off.\n        s5 += s17 * 666643;\n        s6 += s17 * 470296;\n        s7 += s17 * 654183;\n        s8 -= s17 * 997805;\n        s9 += s17 * 136657;\n        s10 -= s17 * 683901;\n        // s17 = 0;\n\n        s4 += s16 * 666643;\n        s5 += s16 * 470296;\n        s6 += s16 * 654183;\n        s7 -= s16 * 997805;\n        s8 += s16 * 136657;\n        s9 -= s16 * 683901;\n        // s16 = 0;\n\n        s3 += s15 * 666643;\n        s4 += s15 * 470296;\n        s5 += s15 * 654183;\n        s6 -= s15 * 997805;\n        s7 += s15 * 136657;\n        s8 -= s15 * 683901;\n        // s15 = 0;\n\n        s2 += s14 * 666643;\n        s3 += s14 * 470296;\n        s4 += s14 * 654183;\n        s5 -= s14 * 997805;\n        s6 += s14 * 136657;\n        s7 -= s14 * 683901;\n        // s14 = 0;\n\n        s1 += s13 * 666643;\n        s2 += s13 * 470296;\n        s3 += s13 * 654183;\n        s4 -= s13 * 997805;\n        s5 += s13 * 136657;\n        s6 -= s13 * 683901;\n        // s13 = 0;\n\n        s0 += s12 * 666643;\n        s1 += s12 * 470296;\n        s2 += s12 * 654183;\n        s3 -= s12 * 997805;\n        s4 += s12 * 136657;\n        s5 -= s12 * 683901;\n        s12 = 0;\n\n        // Reduce the range of limbs from s0 to s11 to 21-bits.\n        carry0 = (s0 + (1 << 20)) >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry2 = (s2 + (1 << 20)) >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry4 = (s4 + (1 << 20)) >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry6 = (s6 + (1 << 20)) >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry8 = (s8 + (1 << 20)) >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry10 = (s10 + (1 << 20)) >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n\n        carry1 = (s1 + (1 << 20)) >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry3 = (s3 + (1 << 20)) >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry5 = (s5 + (1 << 20)) >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry7 = (s7 + (1 << 20)) >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry9 = (s9 + (1 << 20)) >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry11 = (s11 + (1 << 20)) >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n\n        s0 += s12 * 666643;\n        s1 += s12 * 470296;\n        s2 += s12 * 654183;\n        s3 -= s12 * 997805;\n        s4 += s12 * 136657;\n        s5 -= s12 * 683901;\n        s12 = 0;\n\n        // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs.\n        carry0 = s0 >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry1 = s1 >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry2 = s2 >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry3 = s3 >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry4 = s4 >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry5 = s5 >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry6 = s6 >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry7 = s7 >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry8 = s8 >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry9 = s9 >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry10 = s10 >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n        carry11 = s11 >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n\n        // Do one last reduction as s12 might be 1.\n        s0 += s12 * 666643;\n        s1 += s12 * 470296;\n        s2 += s12 * 654183;\n        s3 -= s12 * 997805;\n        s4 += s12 * 136657;\n        s5 -= s12 * 683901;\n        // s12 = 0;\n\n        carry0 = s0 >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry1 = s1 >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry2 = s2 >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry3 = s3 >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry4 = s4 >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry5 = s5 >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry6 = s6 >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry7 = s7 >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry8 = s8 >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry9 = s9 >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry10 = s10 >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n\n        // Serialize the result into the s.\n        s[0] = (byte) s0;\n        s[1] = (byte) (s0 >> 8);\n        s[2] = (byte) ((s0 >> 16) | (s1 << 5));\n        s[3] = (byte) (s1 >> 3);\n        s[4] = (byte) (s1 >> 11);\n        s[5] = (byte) ((s1 >> 19) | (s2 << 2));\n        s[6] = (byte) (s2 >> 6);\n        s[7] = (byte) ((s2 >> 14) | (s3 << 7));\n        s[8] = (byte) (s3 >> 1);\n        s[9] = (byte) (s3 >> 9);\n        s[10] = (byte) ((s3 >> 17) | (s4 << 4));\n        s[11] = (byte) (s4 >> 4);\n        s[12] = (byte) (s4 >> 12);\n        s[13] = (byte) ((s4 >> 20) | (s5 << 1));\n        s[14] = (byte) (s5 >> 7);\n        s[15] = (byte) ((s5 >> 15) | (s6 << 6));\n        s[16] = (byte) (s6 >> 2);\n        s[17] = (byte) (s6 >> 10);\n        s[18] = (byte) ((s6 >> 18) | (s7 << 3));\n        s[19] = (byte) (s7 >> 5);\n        s[20] = (byte) (s7 >> 13);\n        s[21] = (byte) s8;\n        s[22] = (byte) (s8 >> 8);\n        s[23] = (byte) ((s8 >> 16) | (s9 << 5));\n        s[24] = (byte) (s9 >> 3);\n        s[25] = (byte) (s9 >> 11);\n        s[26] = (byte) ((s9 >> 19) | (s10 << 2));\n        s[27] = (byte) (s10 >> 6);\n        s[28] = (byte) ((s10 >> 14) | (s11 << 7));\n        s[29] = (byte) (s11 >> 1);\n        s[30] = (byte) (s11 >> 9);\n        s[31] = (byte) (s11 >> 17);\n    }\n\n    /**\n     * Input:\n     * a[0]+256*a[1]+...+256^31*a[31] = a\n     * b[0]+256*b[1]+...+256^31*b[31] = b\n     * c[0]+256*c[1]+...+256^31*c[31] = c\n     * <p>\n     * Output:\n     * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l\n     * where l = 2^252 + 27742317777372353535851937790883648493.\n     */\n    private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) {\n        // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c\n        // See Ed25519.reduce for related comments.\n        long a0 = 2097151 & load3(a, 0);\n        long a1 = 2097151 & (load4(a, 2) >> 5);\n        long a2 = 2097151 & (load3(a, 5) >> 2);\n        long a3 = 2097151 & (load4(a, 7) >> 7);\n        long a4 = 2097151 & (load4(a, 10) >> 4);\n        long a5 = 2097151 & (load3(a, 13) >> 1);\n        long a6 = 2097151 & (load4(a, 15) >> 6);\n        long a7 = 2097151 & (load3(a, 18) >> 3);\n        long a8 = 2097151 & load3(a, 21);\n        long a9 = 2097151 & (load4(a, 23) >> 5);\n        long a10 = 2097151 & (load3(a, 26) >> 2);\n        long a11 = (load4(a, 28) >> 7);\n        long b0 = 2097151 & load3(b, 0);\n        long b1 = 2097151 & (load4(b, 2) >> 5);\n        long b2 = 2097151 & (load3(b, 5) >> 2);\n        long b3 = 2097151 & (load4(b, 7) >> 7);\n        long b4 = 2097151 & (load4(b, 10) >> 4);\n        long b5 = 2097151 & (load3(b, 13) >> 1);\n        long b6 = 2097151 & (load4(b, 15) >> 6);\n        long b7 = 2097151 & (load3(b, 18) >> 3);\n        long b8 = 2097151 & load3(b, 21);\n        long b9 = 2097151 & (load4(b, 23) >> 5);\n        long b10 = 2097151 & (load3(b, 26) >> 2);\n        long b11 = (load4(b, 28) >> 7);\n        long c0 = 2097151 & load3(c, 0);\n        long c1 = 2097151 & (load4(c, 2) >> 5);\n        long c2 = 2097151 & (load3(c, 5) >> 2);\n        long c3 = 2097151 & (load4(c, 7) >> 7);\n        long c4 = 2097151 & (load4(c, 10) >> 4);\n        long c5 = 2097151 & (load3(c, 13) >> 1);\n        long c6 = 2097151 & (load4(c, 15) >> 6);\n        long c7 = 2097151 & (load3(c, 18) >> 3);\n        long c8 = 2097151 & load3(c, 21);\n        long c9 = 2097151 & (load4(c, 23) >> 5);\n        long c10 = 2097151 & (load3(c, 26) >> 2);\n        long c11 = (load4(c, 28) >> 7);\n        long s0;\n        long s1;\n        long s2;\n        long s3;\n        long s4;\n        long s5;\n        long s6;\n        long s7;\n        long s8;\n        long s9;\n        long s10;\n        long s11;\n        long s12;\n        long s13;\n        long s14;\n        long s15;\n        long s16;\n        long s17;\n        long s18;\n        long s19;\n        long s20;\n        long s21;\n        long s22;\n        long s23;\n        long carry0;\n        long carry1;\n        long carry2;\n        long carry3;\n        long carry4;\n        long carry5;\n        long carry6;\n        long carry7;\n        long carry8;\n        long carry9;\n        long carry10;\n        long carry11;\n        long carry12;\n        long carry13;\n        long carry14;\n        long carry15;\n        long carry16;\n        long carry17;\n        long carry18;\n        long carry19;\n        long carry20;\n        long carry21;\n        long carry22;\n\n        s0 = c0 + a0 * b0;\n        s1 = c1 + a0 * b1 + a1 * b0;\n        s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;\n        s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;\n        s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;\n        s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;\n        s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;\n        s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;\n        s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1\n                + a8 * b0;\n        s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2\n                + a8 * b1 + a9 * b0;\n        s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3\n                + a8 * b2 + a9 * b1 + a10 * b0;\n        s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4\n                + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;\n        s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3\n                + a10 * b2 + a11 * b1;\n        s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3\n                + a11 * b2;\n        s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4\n                + a11 * b3;\n        s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;\n        s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;\n        s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;\n        s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;\n        s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;\n        s20 = a9 * b11 + a10 * b10 + a11 * b9;\n        s21 = a10 * b11 + a11 * b10;\n        s22 = a11 * b11;\n        s23 = 0;\n\n        carry0 = (s0 + (1 << 20)) >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry2 = (s2 + (1 << 20)) >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry4 = (s4 + (1 << 20)) >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry6 = (s6 + (1 << 20)) >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry8 = (s8 + (1 << 20)) >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry10 = (s10 + (1 << 20)) >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n        carry12 = (s12 + (1 << 20)) >> 21;\n        s13 += carry12;\n        s12 -= carry12 << 21;\n        carry14 = (s14 + (1 << 20)) >> 21;\n        s15 += carry14;\n        s14 -= carry14 << 21;\n        carry16 = (s16 + (1 << 20)) >> 21;\n        s17 += carry16;\n        s16 -= carry16 << 21;\n        carry18 = (s18 + (1 << 20)) >> 21;\n        s19 += carry18;\n        s18 -= carry18 << 21;\n        carry20 = (s20 + (1 << 20)) >> 21;\n        s21 += carry20;\n        s20 -= carry20 << 21;\n        carry22 = (s22 + (1 << 20)) >> 21;\n        s23 += carry22;\n        s22 -= carry22 << 21;\n\n        carry1 = (s1 + (1 << 20)) >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry3 = (s3 + (1 << 20)) >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry5 = (s5 + (1 << 20)) >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry7 = (s7 + (1 << 20)) >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry9 = (s9 + (1 << 20)) >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry11 = (s11 + (1 << 20)) >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n        carry13 = (s13 + (1 << 20)) >> 21;\n        s14 += carry13;\n        s13 -= carry13 << 21;\n        carry15 = (s15 + (1 << 20)) >> 21;\n        s16 += carry15;\n        s15 -= carry15 << 21;\n        carry17 = (s17 + (1 << 20)) >> 21;\n        s18 += carry17;\n        s17 -= carry17 << 21;\n        carry19 = (s19 + (1 << 20)) >> 21;\n        s20 += carry19;\n        s19 -= carry19 << 21;\n        carry21 = (s21 + (1 << 20)) >> 21;\n        s22 += carry21;\n        s21 -= carry21 << 21;\n\n        s11 += s23 * 666643;\n        s12 += s23 * 470296;\n        s13 += s23 * 654183;\n        s14 -= s23 * 997805;\n        s15 += s23 * 136657;\n        s16 -= s23 * 683901;\n        // s23 = 0;\n\n        s10 += s22 * 666643;\n        s11 += s22 * 470296;\n        s12 += s22 * 654183;\n        s13 -= s22 * 997805;\n        s14 += s22 * 136657;\n        s15 -= s22 * 683901;\n        // s22 = 0;\n\n        s9 += s21 * 666643;\n        s10 += s21 * 470296;\n        s11 += s21 * 654183;\n        s12 -= s21 * 997805;\n        s13 += s21 * 136657;\n        s14 -= s21 * 683901;\n        // s21 = 0;\n\n        s8 += s20 * 666643;\n        s9 += s20 * 470296;\n        s10 += s20 * 654183;\n        s11 -= s20 * 997805;\n        s12 += s20 * 136657;\n        s13 -= s20 * 683901;\n        // s20 = 0;\n\n        s7 += s19 * 666643;\n        s8 += s19 * 470296;\n        s9 += s19 * 654183;\n        s10 -= s19 * 997805;\n        s11 += s19 * 136657;\n        s12 -= s19 * 683901;\n        // s19 = 0;\n\n        s6 += s18 * 666643;\n        s7 += s18 * 470296;\n        s8 += s18 * 654183;\n        s9 -= s18 * 997805;\n        s10 += s18 * 136657;\n        s11 -= s18 * 683901;\n        // s18 = 0;\n\n        carry6 = (s6 + (1 << 20)) >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry8 = (s8 + (1 << 20)) >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry10 = (s10 + (1 << 20)) >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n        carry12 = (s12 + (1 << 20)) >> 21;\n        s13 += carry12;\n        s12 -= carry12 << 21;\n        carry14 = (s14 + (1 << 20)) >> 21;\n        s15 += carry14;\n        s14 -= carry14 << 21;\n        carry16 = (s16 + (1 << 20)) >> 21;\n        s17 += carry16;\n        s16 -= carry16 << 21;\n\n        carry7 = (s7 + (1 << 20)) >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry9 = (s9 + (1 << 20)) >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry11 = (s11 + (1 << 20)) >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n        carry13 = (s13 + (1 << 20)) >> 21;\n        s14 += carry13;\n        s13 -= carry13 << 21;\n        carry15 = (s15 + (1 << 20)) >> 21;\n        s16 += carry15;\n        s15 -= carry15 << 21;\n\n        s5 += s17 * 666643;\n        s6 += s17 * 470296;\n        s7 += s17 * 654183;\n        s8 -= s17 * 997805;\n        s9 += s17 * 136657;\n        s10 -= s17 * 683901;\n        // s17 = 0;\n\n        s4 += s16 * 666643;\n        s5 += s16 * 470296;\n        s6 += s16 * 654183;\n        s7 -= s16 * 997805;\n        s8 += s16 * 136657;\n        s9 -= s16 * 683901;\n        // s16 = 0;\n\n        s3 += s15 * 666643;\n        s4 += s15 * 470296;\n        s5 += s15 * 654183;\n        s6 -= s15 * 997805;\n        s7 += s15 * 136657;\n        s8 -= s15 * 683901;\n        // s15 = 0;\n\n        s2 += s14 * 666643;\n        s3 += s14 * 470296;\n        s4 += s14 * 654183;\n        s5 -= s14 * 997805;\n        s6 += s14 * 136657;\n        s7 -= s14 * 683901;\n        // s14 = 0;\n\n        s1 += s13 * 666643;\n        s2 += s13 * 470296;\n        s3 += s13 * 654183;\n        s4 -= s13 * 997805;\n        s5 += s13 * 136657;\n        s6 -= s13 * 683901;\n        // s13 = 0;\n\n        s0 += s12 * 666643;\n        s1 += s12 * 470296;\n        s2 += s12 * 654183;\n        s3 -= s12 * 997805;\n        s4 += s12 * 136657;\n        s5 -= s12 * 683901;\n        s12 = 0;\n\n        carry0 = (s0 + (1 << 20)) >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry2 = (s2 + (1 << 20)) >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry4 = (s4 + (1 << 20)) >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry6 = (s6 + (1 << 20)) >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry8 = (s8 + (1 << 20)) >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry10 = (s10 + (1 << 20)) >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n\n        carry1 = (s1 + (1 << 20)) >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry3 = (s3 + (1 << 20)) >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry5 = (s5 + (1 << 20)) >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry7 = (s7 + (1 << 20)) >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry9 = (s9 + (1 << 20)) >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry11 = (s11 + (1 << 20)) >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n\n        s0 += s12 * 666643;\n        s1 += s12 * 470296;\n        s2 += s12 * 654183;\n        s3 -= s12 * 997805;\n        s4 += s12 * 136657;\n        s5 -= s12 * 683901;\n        s12 = 0;\n\n        carry0 = s0 >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry1 = s1 >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry2 = s2 >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry3 = s3 >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry4 = s4 >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry5 = s5 >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry6 = s6 >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry7 = s7 >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry8 = s8 >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry9 = s9 >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry10 = s10 >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n        carry11 = s11 >> 21;\n        s12 += carry11;\n        s11 -= carry11 << 21;\n\n        s0 += s12 * 666643;\n        s1 += s12 * 470296;\n        s2 += s12 * 654183;\n        s3 -= s12 * 997805;\n        s4 += s12 * 136657;\n        s5 -= s12 * 683901;\n        // s12 = 0;\n\n        carry0 = s0 >> 21;\n        s1 += carry0;\n        s0 -= carry0 << 21;\n        carry1 = s1 >> 21;\n        s2 += carry1;\n        s1 -= carry1 << 21;\n        carry2 = s2 >> 21;\n        s3 += carry2;\n        s2 -= carry2 << 21;\n        carry3 = s3 >> 21;\n        s4 += carry3;\n        s3 -= carry3 << 21;\n        carry4 = s4 >> 21;\n        s5 += carry4;\n        s4 -= carry4 << 21;\n        carry5 = s5 >> 21;\n        s6 += carry5;\n        s5 -= carry5 << 21;\n        carry6 = s6 >> 21;\n        s7 += carry6;\n        s6 -= carry6 << 21;\n        carry7 = s7 >> 21;\n        s8 += carry7;\n        s7 -= carry7 << 21;\n        carry8 = s8 >> 21;\n        s9 += carry8;\n        s8 -= carry8 << 21;\n        carry9 = s9 >> 21;\n        s10 += carry9;\n        s9 -= carry9 << 21;\n        carry10 = s10 >> 21;\n        s11 += carry10;\n        s10 -= carry10 << 21;\n\n        s[0] = (byte) s0;\n        s[1] = (byte) (s0 >> 8);\n        s[2] = (byte) ((s0 >> 16) | (s1 << 5));\n        s[3] = (byte) (s1 >> 3);\n        s[4] = (byte) (s1 >> 11);\n        s[5] = (byte) ((s1 >> 19) | (s2 << 2));\n        s[6] = (byte) (s2 >> 6);\n        s[7] = (byte) ((s2 >> 14) | (s3 << 7));\n        s[8] = (byte) (s3 >> 1);\n        s[9] = (byte) (s3 >> 9);\n        s[10] = (byte) ((s3 >> 17) | (s4 << 4));\n        s[11] = (byte) (s4 >> 4);\n        s[12] = (byte) (s4 >> 12);\n        s[13] = (byte) ((s4 >> 20) | (s5 << 1));\n        s[14] = (byte) (s5 >> 7);\n        s[15] = (byte) ((s5 >> 15) | (s6 << 6));\n        s[16] = (byte) (s6 >> 2);\n        s[17] = (byte) (s6 >> 10);\n        s[18] = (byte) ((s6 >> 18) | (s7 << 3));\n        s[19] = (byte) (s7 >> 5);\n        s[20] = (byte) (s7 >> 13);\n        s[21] = (byte) s8;\n        s[22] = (byte) (s8 >> 8);\n        s[23] = (byte) ((s8 >> 16) | (s9 << 5));\n        s[24] = (byte) (s9 >> 3);\n        s[25] = (byte) (s9 >> 11);\n        s[26] = (byte) ((s9 >> 19) | (s10 << 2));\n        s[27] = (byte) (s10 >> 6);\n        s[28] = (byte) ((s10 >> 14) | (s11 << 7));\n        s[29] = (byte) (s11 >> 1);\n        s[30] = (byte) (s11 >> 9);\n        s[31] = (byte) (s11 >> 17);\n    }\n\n    // The order of the generator as unsigned bytes in little endian order.\n    // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748)\n    private static final byte[] GROUP_ORDER = {\n            (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c,\n            (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,\n            (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2,\n            (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,\n            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10};\n\n    // Checks whether s represents an integer smaller than the order of the group.\n    // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check\n    // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.)\n    // @param s an integer in little-endian order.\n    private static boolean isSmallerThanGroupOrder(byte[] s) {\n        for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) {\n            // compare unsigned bytes\n            int a = s[j] & 0xff;\n            int b = GROUP_ORDER[j] & 0xff;\n            if (a != b) {\n                return a < b;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with\n     * {@code publicKey}.\n     */\n    public static boolean verify(final byte[] message, final byte[] signature,\n                                 final byte[] publicKey) {\n        try {\n            if (signature.length != SIGNATURE_LEN) {\n                return false;\n            }\n            if (publicKey.length != PUBLIC_KEY_LEN) {\n                return false;\n            }\n            byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN);\n            if (!isSmallerThanGroupOrder(s)) {\n                return false;\n            }\n            MessageDigest digest = MessageDigest.getInstance(\"SHA-512\");\n            digest.update(signature, 0, Field25519.FIELD_LEN);\n            digest.update(publicKey);\n            digest.update(message);\n            byte[] h = digest.digest();\n            reduce(h);\n\n            XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey);\n            XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s);\n            byte[] expectedR = xyz.toBytes();\n            for (int i = 0; i < Field25519.FIELD_LEN; i++) {\n                if (expectedR[i] != signature[i]) {\n                    return false;\n                }\n            }\n            return true;\n        } catch (final GeneralSecurityException ignored) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "library/include/src/main/java/com.wireguard/crypto/Key.java",
    "content": "/*\n * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage com.wireguard.crypto;\n\nimport com.wireguard.crypto.KeyFormatException.Type;\n\nimport java.security.MessageDigest;\nimport java.security.SecureRandom;\nimport java.util.Arrays;\n\n/**\n * Represents a WireGuard public or private key. This class uses specialized constant-time base64\n * and hexadecimal codec implementations that resist side-channel attacks.\n * <p>\n * Instances of this class are immutable.\n */\n@SuppressWarnings(\"MagicNumber\")\npublic final class Key {\n    private final byte[] key;\n\n    /**\n     * Constructs an object encapsulating the supplied key.\n     *\n     * @param key an array of bytes containing a binary key. Callers of this constructor are\n     *            responsible for ensuring that the array is of the correct length.\n     */\n    private Key(final byte[] key) {\n        // Defensively copy to ensure immutability.\n        this.key = Arrays.copyOf(key, key.length);\n    }\n\n    /**\n     * Decodes a single 4-character base64 chunk to an integer in constant time.\n     *\n     * @param src       an array of at least 4 characters in base64 format\n     * @param srcOffset the offset of the beginning of the chunk in {@code src}\n     * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not\n     * valid base64\n     */\n    private static int decodeBase64(final char[] src, final int srcOffset) {\n        int val = 0;\n        for (int i = 0; i < 4; ++i) {\n            final char c = src[i + srcOffset];\n            val |= (-1\n                    + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))\n                    + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))\n                    + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))\n                    + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)\n                    + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)\n            ) << (18 - 6 * i);\n        }\n        return val;\n    }\n\n    /**\n     * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.\n     *\n     * @param src        an array of at least 3 bytes\n     * @param srcOffset  the offset of the beginning of the chunk in {@code src}\n     * @param dest       an array of at least 4 characters\n     * @param destOffset the offset of the beginning of the chunk in {@code dest}\n     */\n    private static void encodeBase64(final byte[] src, final int srcOffset,\n                                     final char[] dest, final int destOffset) {\n        final byte[] input = {\n                (byte) ((src[srcOffset] >>> 2) & 63),\n                (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),\n                (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),\n                (byte) ((src[2 + srcOffset]) & 63),\n        };\n        for (int i = 0; i < 4; ++i) {\n            dest[i + destOffset] = (char) (input[i] + 'A'\n                    + (((25 - input[i]) >>> 8) & 6)\n                    - (((51 - input[i]) >>> 8) & 75)\n                    - (((61 - input[i]) >>> 8) & 15)\n                    + (((62 - input[i]) >>> 8) & 3));\n        }\n    }\n\n    /**\n     * Decodes a WireGuard public or private key from its base64 string representation. This\n     * function throws a {@link KeyFormatException} if the source string is not well-formed.\n     *\n     * @param str the base64 string representation of a WireGuard key\n     * @return the decoded key encapsulated in an immutable container\n     */\n    public static Key fromBase64(final String str) throws KeyFormatException {\n        final char[] input = str.toCharArray();\n        if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')\n            throw new KeyFormatException(Format.BASE64, Type.LENGTH);\n        final byte[] key = new byte[Format.BINARY.length];\n        int i;\n        int ret = 0;\n        for (i = 0; i < key.length / 3; ++i) {\n            final int val = decodeBase64(input, i * 4);\n            ret |= val >>> 31;\n            key[i * 3] = (byte) ((val >>> 16) & 0xff);\n            key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);\n            key[i * 3 + 2] = (byte) (val & 0xff);\n        }\n        final char[] endSegment = {\n                input[i * 4],\n                input[i * 4 + 1],\n                input[i * 4 + 2],\n                'A',\n        };\n        final int val = decodeBase64(endSegment, 0);\n        ret |= (val >>> 31) | (val & 0xff);\n        key[i * 3] = (byte) ((val >>> 16) & 0xff);\n        key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);\n\n        if (ret != 0)\n            throw new KeyFormatException(Format.BASE64, Type.CONTENTS);\n        return new Key(key);\n    }\n\n    /**\n     * Wraps a WireGuard public or private key in an immutable container. This function throws a\n     * {@link KeyFormatException} if the source data is not the correct length.\n     *\n     * @param bytes an array of bytes containing a WireGuard key in binary format\n     * @return the key encapsulated in an immutable container\n     */\n    public static Key fromBytes(final byte[] bytes) throws KeyFormatException {\n        if (bytes.length != Format.BINARY.length)\n            throw new KeyFormatException(Format.BINARY, Type.LENGTH);\n        return new Key(bytes);\n    }\n\n    /**\n     * Decodes a WireGuard public or private key from its hexadecimal string representation. This\n     * function throws a {@link KeyFormatException} if the source string is not well-formed.\n     *\n     * @param str the hexadecimal string representation of a WireGuard key\n     * @return the decoded key encapsulated in an immutable container\n     */\n    public static Key fromHex(final String str) throws KeyFormatException {\n        final char[] input = str.toCharArray();\n        if (input.length != Format.HEX.length)\n            throw new KeyFormatException(Format.HEX, Type.LENGTH);\n        final byte[] key = new byte[Format.BINARY.length];\n        int ret = 0;\n        for (int i = 0; i < key.length; ++i) {\n            int c;\n            int cNum;\n            int cNum0;\n            int cAlpha;\n            int cAlpha0;\n            int cVal;\n            final int cAcc;\n\n            c = input[i * 2];\n            cNum = c ^ 48;\n            cNum0 = ((cNum - 10) >>> 8) & 0xff;\n            cAlpha = (c & ~32) - 55;\n            cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;\n            ret |= ((cNum0 | cAlpha0) - 1) >>> 8;\n            cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);\n            cAcc = cVal * 16;\n\n            c = input[i * 2 + 1];\n            cNum = c ^ 48;\n            cNum0 = ((cNum - 10) >>> 8) & 0xff;\n            cAlpha = (c & ~32) - 55;\n            cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;\n            ret |= ((cNum0 | cAlpha0) - 1) >>> 8;\n            cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);\n            key[i] = (byte) (cAcc | cVal);\n        }\n        if (ret != 0)\n            throw new KeyFormatException(Format.HEX, Type.CONTENTS);\n        return new Key(key);\n    }\n\n    /**\n     * Generates a private key using the system's {@link SecureRandom} number generator.\n     *\n     * @return a well-formed random private key\n     */\n    static Key generatePrivateKey() {\n        final SecureRandom secureRandom = new SecureRandom();\n        final byte[] privateKey = new byte[Format.BINARY.getLength()];\n        secureRandom.nextBytes(privateKey);\n        privateKey[0] &= 248;\n        privateKey[31] &= 127;\n        privateKey[31] |= 64;\n        return new Key(privateKey);\n    }\n\n    /**\n     * Generates a public key from an existing private key.\n     *\n     * @param privateKey a private key\n     * @return a well-formed public key that corresponds to the supplied private key\n     */\n    static Key generatePublicKey(final Key privateKey) {\n        final byte[] publicKey = new byte[Format.BINARY.getLength()];\n        Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);\n        return new Key(publicKey);\n    }\n\n    @Override\n    public boolean equals(final Object obj) {\n        if (obj == this)\n            return true;\n        if (obj == null || obj.getClass() != getClass())\n            return false;\n        final Key other = (Key) obj;\n        return MessageDigest.isEqual(key, other.key);\n    }\n\n    /**\n     * Returns the key as an array of bytes.\n     *\n     * @return an array of bytes containing the raw binary key\n     */\n    public byte[] getBytes() {\n        // Defensively copy to ensure immutability.\n        return Arrays.copyOf(key, key.length);\n    }\n\n    @Override\n    public int hashCode() {\n        int ret = 0;\n        for (int i = 0; i < key.length / 4; ++i)\n            ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);\n        return ret;\n    }\n\n    /**\n     * Encodes the key to base64.\n     *\n     * @return a string containing the encoded key\n     */\n    public String toBase64() {\n        final char[] output = new char[Format.BASE64.length];\n        int i;\n        for (i = 0; i < key.length / 3; ++i)\n            encodeBase64(key, i * 3, output, i * 4);\n        final byte[] endSegment = {\n                key[i * 3],\n                key[i * 3 + 1],\n                0,\n        };\n        encodeBase64(endSegment, 0, output, i * 4);\n        output[Format.BASE64.length - 1] = '=';\n        return new String(output);\n    }\n\n    /**\n     * Encodes the key to hexadecimal ASCII characters.\n     *\n     * @return a string containing the encoded key\n     */\n    public String toHex() {\n        final char[] output = new char[Format.HEX.length];\n        for (int i = 0; i < key.length; ++i) {\n            output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)\n                    + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));\n            output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)\n                    + ((((key[i] & 0xf) - 10) >> 8) & ~38));\n        }\n        return new String(output);\n    }\n\n    /**\n     * The supported formats for encoding a WireGuard key.\n     */\n    public enum Format {\n        BASE64(44),\n        BINARY(32),\n        HEX(64);\n\n        private final int length;\n\n        Format(final int length) {\n            this.length = length;\n        }\n\n        public int getLength() {\n            return length;\n        }\n    }\n\n}\n"
  },
  {
    "path": "library/include/src/main/java/com.wireguard/crypto/KeyFormatException.java",
    "content": "/*\n * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage com.wireguard.crypto;\n\n/**\n * An exception thrown when attempting to parse an invalid key (too short, too long, or byte\n * data inappropriate for the format). The format being parsed can be accessed with the\n * {@link #getFormat} method.\n */\npublic final class KeyFormatException extends Exception {\n    private final Key.Format format;\n    private final Type type;\n\n    KeyFormatException(final Key.Format format, final Type type) {\n        this.format = format;\n        this.type = type;\n    }\n\n    public Key.Format getFormat() {\n        return format;\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    public enum Type {\n        CONTENTS,\n        LENGTH\n    }\n}\n"
  },
  {
    "path": "library/include/src/main/java/com.wireguard/crypto/KeyPair.java",
    "content": "/*\n * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\npackage com.wireguard.crypto;\n\n/**\n * Represents a Curve25519 key pair as used by WireGuard.\n * <p>\n * Instances of this class are immutable.\n */\npublic class KeyPair {\n    private final Key privateKey;\n    private final Key publicKey;\n\n    /**\n     * Creates a key pair using a newly-generated private key.\n     */\n    public KeyPair() {\n        this(Key.generatePrivateKey());\n    }\n\n    /**\n     * Creates a key pair using an existing private key.\n     *\n     * @param privateKey a private key, used to derive the public key\n     */\n    public KeyPair(final Key privateKey) {\n        this.privateKey = privateKey;\n        publicKey = Key.generatePublicKey(privateKey);\n    }\n\n    /**\n     * Returns the private key from the key pair.\n     *\n     * @return the private key\n     */\n    public Key getPrivateKey() {\n        return privateKey;\n    }\n\n    /**\n     * Returns the public key from the key pair.\n     *\n     * @return the public key\n     */\n    public Key getPublicKey() {\n        return publicKey;\n    }\n}\n"
  },
  {
    "path": "library/include/src/main/java/java/nio/charset/StandardCharsets.java",
    "content": "/*\n * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.\n * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n *\n * This code is free software; you can redistribute it and/or modify it\n * under the terms of the GNU General Public License version 2 only, as\n * published by the Free Software Foundation.  Oracle designates this\n * particular file as subject to the \"Classpath\" exception as provided\n * by Oracle in the LICENSE file that accompanied this code.\n *\n * This code is distributed in the hope that it will be useful, but WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n * version 2 for more details (a copy is included in the LICENSE file that\n * accompanied this code).\n *\n * You should have received a copy of the GNU General Public License version\n * 2 along with this work; if not, write to the Free Software Foundation,\n * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n *\n * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n * or visit www.oracle.com if you need additional information or have any\n * questions.\n */\npackage java.nio.charset;\n\n/**\n * Constant definitions for the standard {@link Charset Charsets}. These\n * charsets are guaranteed to be available on every implementation of the Java\n * platform.\n *\n * @see <a href=\"Charset#standard\">Standard Charsets</a>\n * @since 1.7\n */\npublic final class StandardCharsets {\n\n    private StandardCharsets() {\n        throw new AssertionError(\"No java.nio.charset.StandardCharsets instances for you!\");\n    }\n    /**\n     * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the\n     * Unicode character set\n     */\n    public static final Charset US_ASCII = Charset.forName(\"US-ASCII\");\n    /**\n     * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1\n     */\n    public static final Charset ISO_8859_1 = Charset.forName(\"ISO-8859-1\");\n    /**\n     * Eight-bit UCS Transformation Format\n     */\n    public static final Charset UTF_8 = Charset.forName(\"UTF-8\");\n    /**\n     * Sixteen-bit UCS Transformation Format, big-endian byte order\n     */\n    public static final Charset UTF_16BE = Charset.forName(\"UTF-16BE\");\n    /**\n     * Sixteen-bit UCS Transformation Format, little-endian byte order\n     */\n    public static final Charset UTF_16LE = Charset.forName(\"UTF-16LE\");\n    /**\n     * Sixteen-bit UCS Transformation Format, byte order identified by an\n     * optional byte-order mark\n     */\n    public static final Charset UTF_16 = Charset.forName(\"UTF-16\");\n}\n"
  },
  {
    "path": "library/include/src/main/java/java/nio/file/Path.java",
    "content": "package java.nio.file;\n\npublic interface Path {}\n"
  },
  {
    "path": "library/proto/build.gradle.kts",
    "content": "plugins {\n    `java-library`\n}\n\njava {\n    sourceSets.getByName(\"main\").resources.srcDir(rootProject.file(\"external/Xray-core\"))\n}\n"
  },
  {
    "path": "library/proto-stub/build.gradle.kts",
    "content": "import com.google.protobuf.gradle.*\n\nplugins {\n    id(\"com.android.library\")\n    kotlin(\"android\")\n    id(\"com.google.protobuf\")\n}\n\nsetupKotlinCommon()\n\nval protobufVersion = \"3.18.1\"\n\ndependencies {\n    protobuf(project(\":library:proto\"))\n\n    api(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2\")\n    api(\"com.google.protobuf:protobuf-java:$protobufVersion\")\n}\n\nprotobuf {\n    protoc {\n        artifact = \"com.google.protobuf:protoc:$protobufVersion\"\n    }\n    generateProtoTasks {\n        all().forEach {\n            it.plugins {\n                create(\"java\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "library/proto-stub/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.v2ray.core\" />\n"
  },
  {
    "path": "library/shadowsocks/build.gradle.kts",
    "content": "import com.android.build.gradle.internal.tasks.factory.dependsOn\n\nplugins {\n    id(\"com.android.library\")\n    id(\"org.mozilla.rust-android-gradle.rust-android\")\n}\n\nsetupCommon()\nsetupNdk()\n\ndependencies {\n    implementation(project(\":library:shadowsocks-libev\"))\n}\n\ncargo {\n    module = \"src/main/rust/shadowsocks-rust\"\n    libname = \"sslocal\"\n    val nativeTarget = requireTargetAbi()\n    targets = when (nativeTarget) {\n        \"armeabi-v7a\" -> listOf(\"arm\")\n        \"arm64-v8a\" -> listOf(\"arm64\")\n        \"x86\" -> listOf(\"x86\")\n        \"x86_64\" -> listOf(\"x86_64\")\n        else -> listOf(\"arm\", \"arm64\", \"x86\", \"x86_64\")\n    }\n    profile = findProperty(\"CARGO_PROFILE\")?.toString() ?: \"release\"\n    extraCargoBuildArguments = listOf(\"--bin\", \"sslocal\")\n    featureSpec.noDefaultBut(arrayOf(\n        \"local\",\n        \"stream-cipher\",\n        \"aead-cipher-extra\",\n        \"logging\"))\n    exec = { spec, toolchain ->\n        spec.environment(\"RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY\",\n            \"$projectDir/$module/../linker-wrapper.py\")\n        spec.environment(\"RUST_ANDROID_GRADLE_TARGET\",\n            \"target/${toolchain.target}/$profile/lib$libname.so\")\n    }\n}\n\n\ntasks.whenTaskAdded {\n    if (name.startsWith(\"merge\") && name.endsWith(\"JniLibFolders\")) {\n        dependsOn(\"cargoBuild\")\n    }\n}\n\ntasks.register<Exec>(\"cargoClean\") {\n    executable(\"cargo\")     // cargo.cargoCommand\n    args(\"clean\")\n    workingDir(\"$projectDir/${cargo.module}\")\n}\n\ntasks.clean.dependsOn(\"cargoClean\")"
  },
  {
    "path": "library/shadowsocks/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"io.nekohasekai.ss_rust\">\n\n</manifest>"
  },
  {
    "path": "library/shadowsocks/src/main/rust/linker-wrapper.py",
    "content": "from __future__ import absolute_import, print_function, unicode_literals\n\nimport os\nimport pipes\nimport shutil\nimport subprocess\nimport sys\n\nargs = [os.environ['RUST_ANDROID_GRADLE_CC'], os.environ['RUST_ANDROID_GRADLE_CC_LINK_ARG']] + sys.argv[1:]\n\n# This only appears when the subprocess call fails, but it's helpful then.\nprintable_cmd = ' '.join(pipes.quote(arg) for arg in args)\nprint(printable_cmd)\n\ncode = subprocess.call(args)\nif code == 0:\n    shutil.copyfile(sys.argv[sys.argv.index('-o') + 1], os.environ['RUST_ANDROID_GRADLE_TARGET'])\nsys.exit(code)\n"
  },
  {
    "path": "library/shadowsocks-libev/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n}\n\nsetupNdkLibrary()\n"
  },
  {
    "path": "library/shadowsocks-libev/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"io.nekohasekai.ss_libev\">\n\n</manifest>"
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/Android.mk",
    "content": "# 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#\nLOCAL_PATH := $(call my-dir)\nROOT_PATH := $(LOCAL_PATH)\n\nBUILD_SHARED_EXECUTABLE := $(LOCAL_PATH)/build-shared-executable.mk\n\n########################################################\n## libsodium\n########################################################\n\ninclude $(CLEAR_VARS)\n\nSODIUM_SOURCE := \\\n\tcrypto_aead/aes256gcm/aesni/aead_aes256gcm_aesni.c \\\n\tcrypto_aead/chacha20poly1305/sodium/aead_chacha20poly1305.c \\\n\tcrypto_aead/xchacha20poly1305/sodium/aead_xchacha20poly1305.c \\\n\tcrypto_core/ed25519/ref10/ed25519_ref10.c \\\n\tcrypto_core/hchacha20/core_hchacha20.c \\\n\tcrypto_core/salsa/ref/core_salsa_ref.c \\\n\tcrypto_generichash/blake2b/ref/blake2b-compress-ref.c \\\n\tcrypto_generichash/blake2b/ref/blake2b-ref.c \\\n\tcrypto_generichash/blake2b/ref/generichash_blake2b.c \\\n\tcrypto_onetimeauth/poly1305/onetimeauth_poly1305.c \\\n\tcrypto_onetimeauth/poly1305/donna/poly1305_donna.c \\\n\tcrypto_pwhash/crypto_pwhash.c \\\n\tcrypto_pwhash/argon2/argon2-core.c \\\n\tcrypto_pwhash/argon2/argon2.c \\\n\tcrypto_pwhash/argon2/argon2-encoding.c \\\n\tcrypto_pwhash/argon2/argon2-fill-block-ref.c \\\n\tcrypto_pwhash/argon2/blake2b-long.c \\\n\tcrypto_pwhash/argon2/pwhash_argon2i.c \\\n\tcrypto_scalarmult/curve25519/scalarmult_curve25519.c \\\n\tcrypto_scalarmult/curve25519/ref10/x25519_ref10.c \\\n\tcrypto_stream/chacha20/stream_chacha20.c \\\n\tcrypto_stream/chacha20/ref/chacha20_ref.c \\\n\tcrypto_stream/salsa20/stream_salsa20.c \\\n\tcrypto_stream/salsa20/ref/salsa20_ref.c \\\n\tcrypto_verify/sodium/verify.c \\\n\trandombytes/randombytes.c \\\n\trandombytes/sysrandom/randombytes_sysrandom.c \\\n\tsodium/core.c \\\n\tsodium/runtime.c \\\n\tsodium/utils.c \\\n\tsodium/version.c\n\nLOCAL_MODULE := sodium\nLOCAL_CFLAGS += -I$(LOCAL_PATH)/libsodium/src/libsodium/include \\\n\t\t\t\t-I$(LOCAL_PATH)/include \\\n\t\t\t\t-I$(LOCAL_PATH)/include/sodium \\\n\t\t\t\t-I$(LOCAL_PATH)/libsodium/src/libsodium/include/sodium \\\n\t\t\t\t-DPACKAGE_NAME=\\\"libsodium\\\" -DPACKAGE_TARNAME=\\\"libsodium\\\" \\\n\t\t\t\t-DPACKAGE_VERSION=\\\"1.0.15\\\" -DPACKAGE_STRING=\\\"libsodium-1.0.15\\\" \\\n\t\t\t\t-DPACKAGE_BUGREPORT=\\\"https://github.com/jedisct1/libsodium/issues\\\" \\\n\t\t\t\t-DPACKAGE_URL=\\\"https://github.com/jedisct1/libsodium\\\" \\\n\t\t\t\t-DPACKAGE=\\\"libsodium\\\" -DVERSION=\\\"1.0.15\\\" \\\n\t\t\t\t-DHAVE_PTHREAD=1                  \\\n\t\t\t\t-DSTDC_HEADERS=1                  \\\n\t\t\t\t-DHAVE_SYS_TYPES_H=1              \\\n\t\t\t\t-DHAVE_SYS_STAT_H=1               \\\n\t\t\t\t-DHAVE_STDLIB_H=1                 \\\n\t\t\t\t-DHAVE_STRING_H=1                 \\\n\t\t\t\t-DHAVE_MEMORY_H=1                 \\\n\t\t\t\t-DHAVE_STRINGS_H=1                \\\n\t\t\t\t-DHAVE_INTTYPES_H=1               \\\n\t\t\t\t-DHAVE_STDINT_H=1                 \\\n\t\t\t\t-DHAVE_UNISTD_H=1                 \\\n\t\t\t\t-D__EXTENSIONS__=1                \\\n\t\t\t\t-D_ALL_SOURCE=1                   \\\n\t\t\t\t-D_GNU_SOURCE=1                   \\\n\t\t\t\t-D_POSIX_PTHREAD_SEMANTICS=1      \\\n\t\t\t\t-D_TANDEM_SOURCE=1                \\\n\t\t\t\t-DHAVE_DLFCN_H=1                  \\\n\t\t\t\t-DLT_OBJDIR=\\\".libs/\\\"            \\\n\t\t\t\t-DHAVE_SYS_MMAN_H=1               \\\n\t\t\t\t-DNATIVE_LITTLE_ENDIAN=1          \\\n\t\t\t\t-DASM_HIDE_SYMBOL=.hidden         \\\n\t\t\t\t-DHAVE_WEAK_SYMBOLS=1             \\\n\t\t\t\t-DHAVE_ATOMIC_OPS=1               \\\n\t\t\t\t-DHAVE_ARC4RANDOM=1               \\\n\t\t\t\t-DHAVE_ARC4RANDOM_BUF=1           \\\n\t\t\t\t-DHAVE_MMAP=1                     \\\n\t\t\t\t-DHAVE_MLOCK=1                    \\\n\t\t\t\t-DHAVE_MADVISE=1                  \\\n\t\t\t\t-DHAVE_MPROTECT=1                 \\\n\t\t\t\t-DHAVE_NANOSLEEP=1                \\\n\t\t\t\t-DHAVE_POSIX_MEMALIGN=1           \\\n\t\t\t\t-DHAVE_GETPID=1                   \\\n\t\t\t\t-DCONFIGURED=1\n\nLOCAL_SRC_FILES := $(addprefix libsodium/src/libsodium/,$(SODIUM_SOURCE))\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## libevent\n########################################################\n\ninclude $(CLEAR_VARS)\n\nLIBEVENT_SOURCES := \\\n\tbuffer.c bufferevent.c event.c \\\n\tbufferevent_sock.c bufferevent_ratelim.c \\\n\tevthread.c log.c evutil.c evutil_rand.c evutil_time.c evmap.c epoll.c poll.c signal.c select.c\n\nLOCAL_MODULE := event\nLOCAL_SRC_FILES := $(addprefix libevent/, $(LIBEVENT_SOURCES))\nLOCAL_CFLAGS := -I$(LOCAL_PATH)/libevent \\\n\t-I$(LOCAL_PATH)/libevent/include \\\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## libancillary\n########################################################\n\ninclude $(CLEAR_VARS)\n\nANCILLARY_SOURCE := fd_recv.c fd_send.c\n\nLOCAL_MODULE := libancillary\nLOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary\n\nLOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## libbloom\n########################################################\n\ninclude $(CLEAR_VARS)\n\nBLOOM_SOURCE := bloom.c murmur2/MurmurHash2.c\n\nLOCAL_MODULE := libbloom\nLOCAL_CFLAGS += -I$(LOCAL_PATH)/shadowsocks-libev/libbloom \\\n\t\t\t\t-I$(LOCAL_PATH)/shadowsocks-libev/libbloom/murmur2\n\nLOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libbloom/, $(BLOOM_SOURCE))\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## libipset\n########################################################\n\ninclude $(CLEAR_VARS)\n\nbdd_src = bdd/assignments.c bdd/basics.c bdd/bdd-iterator.c bdd/expanded.c \\\n\t\t  \t\t  bdd/reachable.c bdd/read.c bdd/write.c\nmap_src = map/allocation.c map/inspection.c map/ipv4_map.c map/ipv6_map.c \\\n\t\t  \t\t  map/storage.c\nset_src = set/allocation.c set/inspection.c set/ipv4_set.c set/ipv6_set.c \\\n\t\t  \t\t  set/iterator.c set/storage.c\n\nIPSET_SOURCE := general.c $(bdd_src) $(map_src) $(set_src)\n\nLOCAL_MODULE := libipset\nLOCAL_CFLAGS += -I$(LOCAL_PATH)/shadowsocks-libev/libipset/include \\\n\t\t\t\t-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include\n\nLOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libipset/src/libipset/,$(IPSET_SOURCE))\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## libcork\n########################################################\n\ninclude $(CLEAR_VARS)\n\ncli_src := cli/commands.c\ncore_src := core/allocator.c core/error.c core/gc.c \\\n\t\t\tcore/hash.c core/ip-address.c core/mempool.c \\\n\t\t\tcore/timestamp.c core/u128.c\nds_src := ds/array.c ds/bitset.c ds/buffer.c ds/dllist.c \\\n\t\t  ds/file-stream.c ds/hash-table.c ds/managed-buffer.c \\\n\t\t  ds/ring-buffer.c ds/slice.c\nposix_src := posix/directory-walker.c posix/env.c posix/exec.c \\\n\t\t\t posix/files.c posix/process.c posix/subprocess.c\npthreads_src := pthreads/thread.c\n\nCORK_SOURCE := $(cli_src) $(core_src) $(ds_src) $(posix_src) $(pthreads_src)\n\nLOCAL_MODULE := libcork\nLOCAL_CFLAGS += -I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \\\n\t\t\t\t-DCORK_API=CORK_LOCAL\n\nLOCAL_SRC_FILES := $(addprefix shadowsocks-libev/libcork/src/libcork/,$(CORK_SOURCE))\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## libev\n########################################################\n\ninclude $(CLEAR_VARS)\n\nLOCAL_MODULE := libev\nLOCAL_CFLAGS += -DNDEBUG -DHAVE_CONFIG_H \\\n\t\t\t\t-I$(LOCAL_PATH)/include/libev\nLOCAL_SRC_FILES := \\\n\tlibev/ev.c \\\n\tlibev/event.c\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## shadowsocks-libev local\n########################################################\n\ninclude $(CLEAR_VARS)\n\nSHADOWSOCKS_SOURCES := local.c \\\n\tcache.c udprelay.c utils.c netutils.c json.c jconf.c \\\n\tacl.c rule.c \\\n\tcrypto.c aead.c stream.c base64.c \\\n\tplugin.c ppbloom.c \\\n\tandroid.c\n\nLOCAL_MODULE    := ss-local\nLOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))\nLOCAL_CFLAGS    := -Wall -fno-strict-aliasing -DMODULE_LOCAL \\\n\t\t\t\t\t-DUSE_CRYPTO_MBEDTLS -DHAVE_CONFIG_H \\\n\t\t\t\t\t-DCONNECT_IN_PROGRESS=EINPROGRESS \\\n\t\t\t\t\t-I$(LOCAL_PATH)/include/shadowsocks-libev \\\n\t\t\t\t\t-I$(LOCAL_PATH)/include \\\n\t\t\t\t\t-I$(LOCAL_PATH)/libancillary \\\n\t\t\t\t\t-I$(LOCAL_PATH)/mbedtls/include  \\\n\t\t\t\t\t-I$(LOCAL_PATH)/pcre \\\n\t\t\t\t\t-I$(LOCAL_PATH)/libsodium/src/libsodium/include \\\n\t\t\t\t\t-I$(LOCAL_PATH)/libsodium/src/libsodium/include/sodium \\\n\t\t\t\t\t-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \\\n\t\t\t\t\t-I$(LOCAL_PATH)/shadowsocks-libev/libipset/include \\\n\t\t\t\t\t-I$(LOCAL_PATH)/shadowsocks-libev/libbloom \\\n\t\t\t\t\t-I$(LOCAL_PATH)/libev\n\nLOCAL_STATIC_LIBRARIES := libev libmbedtls libipset libcork libbloom \\\n\tlibsodium libancillary libpcre\n\nLOCAL_LDLIBS := -llog\n\ninclude $(BUILD_SHARED_EXECUTABLE)\n\n########################################################\n## mbed TLS\n########################################################\n\ninclude $(CLEAR_VARS)\n\nLOCAL_MODULE := mbedtls\n\nLOCAL_C_INCLUDES := $(LOCAL_PATH)/mbedtls/include\n\nMBEDTLS_SOURCES := $(wildcard $(LOCAL_PATH)/mbedtls/library/*.c)\n\nLOCAL_SRC_FILES := $(MBEDTLS_SOURCES:$(LOCAL_PATH)/%=%)\n\ninclude $(BUILD_STATIC_LIBRARY)\n\n########################################################\n## pcre\n########################################################\n\ninclude $(CLEAR_VARS)\n\nLOCAL_MODULE := pcre\n\nLOCAL_CFLAGS += -DHAVE_CONFIG_H\n\nLOCAL_C_INCLUDES := $(LOCAL_PATH)/pcre/dist $(LOCAL_PATH)/pcre\n\nlibpcre_src_files := \\\n    dist/pcre_byte_order.c \\\n    dist/pcre_compile.c \\\n    dist/pcre_config.c \\\n    dist/pcre_dfa_exec.c \\\n    dist/pcre_exec.c \\\n    dist/pcre_fullinfo.c \\\n    dist/pcre_get.c \\\n    dist/pcre_globals.c \\\n    dist/pcre_jit_compile.c \\\n    dist/pcre_maketables.c \\\n    dist/pcre_newline.c \\\n    dist/pcre_ord2utf8.c \\\n    dist/pcre_refcount.c \\\n    dist/pcre_string_utils.c \\\n    dist/pcre_study.c \\\n    dist/pcre_tables.c \\\n    dist/pcre_ucd.c \\\n    dist/pcre_valid_utf8.c \\\n    dist/pcre_version.c \\\n    dist/pcre_xclass.c\n\nLOCAL_SRC_FILES := $(addprefix pcre/, $(libpcre_src_files)) $(LOCAL_PATH)/patch/pcre/pcre_chartables.c\n\ninclude $(BUILD_STATIC_LIBRARY)\n"
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/Application.mk",
    "content": ""
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/build-shared-executable.mk",
    "content": "# 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# this file is included from Android.mk files to build a target-specific\n# executable program\n#\n# Modified by @Mygod, based on:\n#   https://android.googlesource.com/platform/ndk/+/a355a4e/build/core/build-shared-library.mk\n#   https://android.googlesource.com/platform/ndk/+/a355a4e/build/core/build-executable.mk\nLOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE\nLOCAL_MAKEFILE     := $(local-makefile)\n$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))\n$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))\n$(call check-LOCAL_MODULE_FILENAME)\n# we are building target objects\nmy := TARGET_\n$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))\n$(call handle-module-built)\nLOCAL_MODULE_CLASS := EXECUTABLE\ninclude $(BUILD_SYSTEM)/build-module.mk\n"
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/include/libev/config.h",
    "content": "/* config.h.  Generated from config.h.in by configure.  */\n/* config.h.in.  Generated from configure.ac by autoheader.  */\n\n/* Define to 1 if you have the `clock_gettime' function. */\n/* #undef HAVE_CLOCK_GETTIME */\n\n/* Define to 1 to use the syscall interface for clock_gettime */\n#define HAVE_CLOCK_SYSCALL 1\n\n/* Define to 1 if you have the <dlfcn.h> header file. */\n#define HAVE_DLFCN_H 1\n\n/* Define to 1 if you have the `epoll_ctl' function. */\n#define HAVE_EPOLL_CTL 1\n\n/* Define to 1 if you have the `eventfd' function. */\n#define HAVE_EVENTFD 1\n\n/* Define to 1 if the floor function is available */\n#define HAVE_FLOOR 1\n\n/* Define to 1 if you have the `inotify_init' function. */\n#define HAVE_INOTIFY_INIT 1\n\n/* Define to 1 if you have the <inttypes.h> header file. */\n#define HAVE_INTTYPES_H 1\n\n/* Define to 1 if you have the `kqueue' function. */\n/* #undef HAVE_KQUEUE */\n\n/* Define to 1 if you have the `rt' library (-lrt). */\n/* #undef HAVE_LIBRT */\n\n/* Define to 1 if you have the <memory.h> header file. */\n#define HAVE_MEMORY_H 1\n\n/* Define to 1 if you have the `nanosleep' function. */\n#define HAVE_NANOSLEEP 1\n\n/* Define to 1 if you have the `poll' function. */\n#define HAVE_POLL 1\n\n/* Define to 1 if you have the <poll.h> header file. */\n#define HAVE_POLL_H 1\n\n/* Define to 1 if you have the `port_create' function. */\n/* #undef HAVE_PORT_CREATE */\n\n/* Define to 1 if you have the <port.h> header file. */\n/* #undef HAVE_PORT_H */\n\n/* Define to 1 if you have the `select' function. */\n#define HAVE_SELECT 1\n\n/* Define to 1 if you have the `signalfd' function. */\n#define HAVE_SIGNALFD 0\n\n/* Define to 1 if you have the <stdint.h> header file. */\n#define HAVE_STDINT_H 1\n\n/* Define to 1 if you have the <stdlib.h> header file. */\n#define HAVE_STDLIB_H 1\n\n/* Define to 1 if you have the <strings.h> header file. */\n#define HAVE_STRINGS_H 1\n\n/* Define to 1 if you have the <string.h> header file. */\n#define HAVE_STRING_H 1\n\n/* Define to 1 if you have the <sys/epoll.h> header file. */\n#define HAVE_SYS_EPOLL_H 1\n\n/* Define to 1 if you have the <sys/eventfd.h> header file. */\n#define HAVE_SYS_EVENTFD_H 1\n\n/* Define to 1 if you have the <sys/event.h> header file. */\n/* #undef HAVE_SYS_EVENT_H */\n\n/* Define to 1 if you have the <sys/inotify.h> header file. */\n#define HAVE_SYS_INOTIFY_H 1\n\n/* Define to 1 if you have the <sys/select.h> header file. */\n#define HAVE_SYS_SELECT_H 1\n\n/* Define to 1 if you have the <sys/signalfd.h> header file. */\n#define HAVE_SYS_SIGNALFD_H 1\n\n/* Define to 1 if you have the <sys/stat.h> header file. */\n#define HAVE_SYS_STAT_H 1\n\n/* Define to 1 if you have the <sys/types.h> header file. */\n#define HAVE_SYS_TYPES_H 1\n\n/* Define to 1 if you have the <unistd.h> header file. */\n#define HAVE_UNISTD_H 1\n\n/* Define to the sub-directory in which libtool stores uninstalled libraries.\n   */\n#define LT_OBJDIR \".libs/\"\n\n/* Name of package */\n#define PACKAGE \"libev\"\n\n/* Define to the address where bug reports for this package should be sent. */\n#define PACKAGE_BUGREPORT \"\"\n\n/* Define to the full name of this package. */\n#define PACKAGE_NAME \"\"\n\n/* Define to the full name and version of this package. */\n#define PACKAGE_STRING \"\"\n\n/* Define to the one symbol short name of this package. */\n#define PACKAGE_TARNAME \"\"\n\n/* Define to the home page for this package. */\n#define PACKAGE_URL \"\"\n\n/* Define to the version of this package. */\n#define PACKAGE_VERSION \"\"\n\n/* Define to 1 if you have the ANSI C header files. */\n#define STDC_HEADERS 1\n\n/* Version number of package */\n#define VERSION \"4.11\"\n"
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/include/shadowsocks-libev/config.h",
    "content": "/* config.h.  Generated from config.h.in by configure.  */\n/* config.h.in.  Generated from configure.ac by autoheader.  */\n\n/* Define if building universal (internal helper macro) */\n/* #undef AC_APPLE_UNIVERSAL_BUILD */\n\n/* errno for incomplete non-blocking connect(2) */\n#define CONNECT_IN_PROGRESS EINPROGRESS\n\n/* Override libev default fd conversion macro. */\n/* #undef EV_FD_TO_WIN32_HANDLE */\n\n/* Override libev default fd close macro. */\n/* #undef EV_WIN32_CLOSE_FD */\n\n/* Override libev default handle conversion macro. */\n/* #undef EV_WIN32_HANDLE_TO_FD */\n\n/* Reset max file descriptor size. */\n/* #undef FD_SETSIZE */\n\n/* Define to 1 if you have the <arpa/inet.h> header file. */\n#define HAVE_ARPA_INET_H 1\n\n/* Define to 1 if you have the `CCCryptorCreateWithMode' function. */\n/* #undef HAVE_CCCRYPTORCREATEWITHMODE */\n\n/* Define to 1 if you have the `clock_gettime' function. */\n/* #undef HAVE_CLOCK_GETTIME */\n\n/* Define to 1 to use the syscall interface for clock_gettime */\n/* #undef HAVE_CLOCK_SYSCALL */\n\n/* Define to 1 if you have the <CommonCrypto/CommonCrypto.h> header file. */\n/* #undef HAVE_COMMONCRYPTO_COMMONCRYPTO_H */\n\n/* Define to 1 if you have the declaration of `inet_ntop', and to 0 if you\n   don't. */\n#define HAVE_DECL_INET_NTOP 1\n\n/* Define to 1 if you have the <dlfcn.h> header file. */\n#define HAVE_DLFCN_H 1\n\n/* Define to 1 if you have the <linux/tcp.h> header file. */\n#define HAVE_LINUX_TCP_H 1\n\n/* Define to 1 if you have the `epoll_ctl' function. */\n/* #undef HAVE_EPOLL_CTL */\n\n/* Define to 1 if you have the `eventfd' function. */\n/* #undef HAVE_EVENTFD */\n\n/* Define to 1 if you have the `EVP_EncryptInit_ex' function. */\n/* #undef HAVE_EVP_ENCRYPTINIT_EX */\n\n/* Define to 1 if you have the <fcntl.h> header file. */\n#define HAVE_FCNTL_H 1\n\n/* Define to 1 if the floor function is available */\n#define HAVE_FLOOR 1\n\n/* Define to 1 if you have the `fork' function. */\n#define HAVE_FORK 1\n\n/* Define to 1 if you have the `getpwnam_r' function. */\n#define HAVE_GETPWNAM_R 1\n\n/* Define to 1 if you have the `inet_ntop' function. */\n/* #undef HAVE_INET_NTOP */\n\n/* Define to 1 if you have the `inotify_init' function. */\n/* #undef HAVE_INOTIFY_INIT */\n\n/* Define to 1 if you have the <inttypes.h> header file. */\n#define HAVE_INTTYPES_H 1\n\n/* Enable IPv6 support in libudns */\n#define HAVE_IPv6 1\n\n/* Define to 1 if you have the `kqueue' function. */\n#define HAVE_KQUEUE 1\n\n/* Define to 1 if you have the <langinfo.h> header file. */\n#define HAVE_LANGINFO_H 1\n\n/* Define to 1 if you have the `rt' library (-lrt). */\n/* #undef HAVE_LIBRT */\n\n/* Define to 1 if you have the `socket' library (-lsocket). */\n/* #undef HAVE_LIBSOCKET */\n\n/* Define to 1 if you have the <limits.h> header file. */\n#define HAVE_LIMITS_H 1\n\n/* Define to 1 if you have the <linux/if.h> header file. */\n/* #undef HAVE_LINUX_IF_H */\n\n/* Define to 1 if you have the <linux/netfilter_ipv4.h> header file. */\n/* #undef HAVE_LINUX_NETFILTER_IPV4_H */\n\n/* Define to 1 if you have the <linux/netfilter_ipv6/ip6_tables.h> header\n   file. */\n/* #undef HAVE_LINUX_NETFILTER_IPV6_IP6_TABLES_H */\n\n/* Define to 1 if you have the <locale.h> header file. */\n#define HAVE_LOCALE_H 1\n\n/* Define to 1 if you have the `malloc' function. */\n#define HAVE_MALLOC 1\n\n/* Define to 1 if you have the <memory.h> header file. */\n#define HAVE_MEMORY_H 1\n\n/* Define to 1 if you have the `memset' function. */\n#define HAVE_MEMSET 1\n\n/* Define to 1 if you have the `nanosleep' function. */\n#define HAVE_NANOSLEEP 1\n\n/* Define to 1 if you have the <netdb.h> header file. */\n#define HAVE_NETDB_H 1\n\n/* Define to 1 if you have the <netinet/in.h> header file. */\n#define HAVE_NETINET_IN_H 1\n\n/* Define to 1 if you have the <net/if.h> header file. */\n#define HAVE_NET_IF_H 1\n\n/* Define to 1 if you have the <openssl/engine.h> header file. */\n/* #undef HAVE_OPENSSL_ENGINE_H */\n\n/* Define to 1 if you have the <openssl/err.h> header file. */\n/* #undef HAVE_OPENSSL_ERR_H */\n\n/* Define to 1 if you have the <openssl/evp.h> header file. */\n/* #undef HAVE_OPENSSL_EVP_H */\n\n/* Define to 1 if you have the <openssl/pem.h> header file. */\n/* #undef HAVE_OPENSSL_PEM_H */\n\n/* Define to 1 if you have the <openssl/rand.h> header file. */\n/* #undef HAVE_OPENSSL_RAND_H */\n\n/* Define to 1 if you have the <openssl/rsa.h> header file. */\n/* #undef HAVE_OPENSSL_RSA_H */\n\n/* Define to 1 if you have the <openssl/sha.h> header file. */\n/* #undef HAVE_OPENSSL_SHA_H */\n\n/* Define to 1 if you have the `poll' function. */\n#define HAVE_POLL 1\n\n/* Define to 1 if you have the <poll.h> header file. */\n#define HAVE_POLL_H 1\n\n/* Define to 1 if you have the `port_create' function. */\n/* #undef HAVE_PORT_CREATE */\n\n/* Define to 1 if you have the <port.h> header file. */\n/* #undef HAVE_PORT_H */\n\n/* Have PTHREAD_PRIO_INHERIT. */\n#define HAVE_PTHREAD_PRIO_INHERIT 1\n\n/* Define to 1 if you have the `RAND_pseudo_bytes' function. */\n/* #undef HAVE_RAND_PSEUDO_BYTES */\n\n/* Define to 1 if you have the 'select' function. */\n#define HAVE_SELECT 1\n\n/* Define to 1 if you have the `setresuid' function. */\n/* #undef HAVE_SETRESUID */\n\n/* Define to 1 if you have the `setreuid' function. */\n#define HAVE_SETREUID 1\n\n/* Define to 1 if you have the `setrlimit' function. */\n#define HAVE_SETRLIMIT 1\n\n/* Define to 1 if you have the `signalfd' function. */\n/* #undef HAVE_SIGNALFD */\n\n/* Define to 1 if you have the `socket' function. */\n#define HAVE_SOCKET 1\n\n/* Define to 1 if you have the <stdint.h> header file. */\n#define HAVE_STDINT_H 1\n\n/* Define to 1 if you have the <stdlib.h> header file. */\n#define HAVE_STDLIB_H 1\n\n/* Define to 1 if you have the `strerror' function. */\n#define HAVE_STRERROR 1\n\n/* Define to 1 if you have the <strings.h> header file. */\n#define HAVE_STRINGS_H 1\n\n/* Define to 1 if you have the <string.h> header file. */\n#define HAVE_STRING_H 1\n\n/* Define to 1 if you have the <sys/epoll.h> header file. */\n/* #undef HAVE_SYS_EPOLL_H */\n\n/* Define to 1 if you have the <sys/eventfd.h> header file. */\n/* #undef HAVE_SYS_EVENTFD_H */\n\n/* Define to 1 if you have the <sys/event.h> header file. */\n#define HAVE_SYS_EVENT_H 1\n\n/* Define to 1 if you have the <sys/inotify.h> header file. */\n/* #undef HAVE_SYS_INOTIFY_H */\n\n/* Define to 1 if you have the <sys/ioctl.h> header file. */\n#define HAVE_SYS_IOCTL_H 1\n\n/* Define to 1 if you have the <sys/select.h> header file. */\n#define HAVE_SYS_SELECT_H 1\n\n/* Define to 1 if you have the <sys/signalfd.h> header file. */\n/* #undef HAVE_SYS_SIGNALFD_H */\n\n/* Define to 1 if you have the <sys/socket.h> header file. */\n#define HAVE_SYS_SOCKET_H 1\n\n/* Define to 1 if you have the <sys/stat.h> header file. */\n#define HAVE_SYS_STAT_H 1\n\n/* Define to 1 if you have the <sys/types.h> header file. */\n#define HAVE_SYS_TYPES_H 1\n\n/* Define to 1 if you have <sys/wait.h> that is POSIX.1 compatible. */\n#define HAVE_SYS_WAIT_H 1\n\n/* Define to 1 if you have the <unistd.h> header file. */\n#define HAVE_UNISTD_H 1\n\n/* Define to 1 if you have the `vfork' function. */\n#define HAVE_VFORK 1\n\n/* Define to 1 if you have the <vfork.h> header file. */\n/* #undef HAVE_VFORK_H */\n\n/* Define to 1 if you have the <windows.h> header file. */\n/* #undef HAVE_WINDOWS_H */\n\n/* Define to 1 if you have the <winsock2.h> header file. */\n/* #undef HAVE_WINSOCK2_H */\n\n/* Define to 1 if `fork' works. */\n#define HAVE_WORKING_FORK 1\n\n/* Define to 1 if `vfork' works. */\n#define HAVE_WORKING_VFORK 1\n\n/* Define to 1 if you have the <ws2tcpip.h> header file. */\n/* #undef HAVE_WS2TCPIP_H */\n\n/* have zlib compression support */\n/* #undef HAVE_ZLIB */\n\n/* Define to 1 if you have the <zlib.h> header file. */\n/* #undef HAVE_ZLIB_H */\n\n/* Define to the sub-directory in which libtool stores uninstalled libraries.\n   */\n#define LT_OBJDIR \".libs/\"\n\n/* Define to 1 if assertions should be disabled. */\n/* #undef NDEBUG */\n\n/* Name of package */\n#define PACKAGE \"shadowsocks-libev\"\n\n/* Define to the address where bug reports for this package should be sent. */\n#define PACKAGE_BUGREPORT \"max.c.lv@gmail.com\"\n\n/* Define to the full name of this package. */\n#define PACKAGE_NAME \"shadowsocks-libev\"\n\n/* Define to the full name and version of this package. */\n#define PACKAGE_STRING \"shadowsocks-libev 2.4.8\"\n\n/* Define to the one symbol short name of this package. */\n#define PACKAGE_TARNAME \"shadowsocks-libev\"\n\n/* Define to the home page for this package. */\n#define PACKAGE_URL \"\"\n\n/* Define to the version of this package. */\n#define PACKAGE_VERSION \"2.4.8\"\n\n/* Define to necessary symbol if this constant uses a non-standard name on\n   your system. */\n/* #undef PTHREAD_CREATE_JOINABLE */\n\n/* Define as the return type of signal handlers (`int' or `void'). */\n#define RETSIGTYPE void\n\n/* Define to the type of arg 1 for `select'. */\n#define SELECT_TYPE_ARG1 int\n\n/* Define to the type of args 2, 3 and 4 for `select'. */\n#define SELECT_TYPE_ARG234 (fd_set *)\n\n/* Define to the type of arg 5 for `select'. */\n#define SELECT_TYPE_ARG5 (struct timeval *)\n\n/* Define to 1 if you have the ANSI C header files. */\n#define STDC_HEADERS 1\n\n/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */\n#define TIME_WITH_SYS_TIME 1\n\n/* If the compiler supports a TLS storage class define it to that here */\n#define TLS __thread\n\n/* Use Apple CommonCrypto library */\n/* #undef USE_CRYPTO_APPLECC */\n\n/* Use mbed TLS library */\n#define USE_CRYPTO_MBEDTLS 1\n\n/* Use OpenSSL library */\n/* #undef USE_CRYPTO_OPENSSL */\n\n/* Use PolarSSL library */\n/* #undef USE_CRYPTO_POLARSSL */\n\n/* Enable extensions on AIX 3, Interix.  */\n#ifndef _ALL_SOURCE\n# define _ALL_SOURCE 1\n#endif\n/* Enable GNU extensions on systems that have them.  */\n#ifndef _GNU_SOURCE\n# define _GNU_SOURCE 1\n#endif\n/* Enable threading extensions on Solaris.  */\n#ifndef _POSIX_PTHREAD_SEMANTICS\n# define _POSIX_PTHREAD_SEMANTICS 1\n#endif\n/* Enable extensions on HP NonStop.  */\n#ifndef _TANDEM_SOURCE\n# define _TANDEM_SOURCE 1\n#endif\n/* Enable general extensions on Solaris.  */\n#ifndef __EXTENSIONS__\n# define __EXTENSIONS__ 1\n#endif\n\n\n/* Version number of package */\n#define VERSION \"2.4.8\"\n\n/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most\n   significant byte first (like Motorola and SPARC, unlike Intel). */\n#if defined AC_APPLE_UNIVERSAL_BUILD\n# if defined __BIG_ENDIAN__\n#  define WORDS_BIGENDIAN 1\n# endif\n#else\n# ifndef WORDS_BIGENDIAN\n/* #  undef WORDS_BIGENDIAN */\n# endif\n#endif\n\n/* Define to 1 if on MINIX. */\n/* #undef _MINIX */\n\n/* Define to 2 if the system does not provide POSIX.1 features except with\n   this defined. */\n/* #undef _POSIX_1_SOURCE */\n\n/* Define to 1 if you need to in order for `stat' and other things to work. */\n/* #undef _POSIX_SOURCE */\n\n/* Define for Solaris 2.5.1 so the uint8_t typedef from <sys/synch.h>,\n   <pthread.h>, or <semaphore.h> is not used. If the typedef were allowed, the\n   #define below would cause a syntax error. */\n/* #undef _UINT8_T */\n\n/* Define to empty if `const' does not conform to ANSI C. */\n/* #undef const */\n\n/* Define to `__inline__' or `__inline' if that's what the C compiler\n   calls it, or to nothing if 'inline' is not supported under any name.  */\n#ifndef __cplusplus\n/* #undef inline */\n#endif\n\n/* Define to `int' if <sys/types.h> does not define. */\n/* #undef pid_t */\n\n/* Define to the equivalent of the C99 'restrict' keyword, or to\n   nothing if this is not supported.  Do not define if restrict is\n   supported directly.  */\n#define restrict __restrict\n/* Work around a bug in Sun C++: it does not support _Restrict or\n   __restrict__, even though the corresponding Sun C compiler ends up with\n   \"#define restrict _Restrict\" or \"#define restrict __restrict__\" in the\n   previous line.  Perhaps some future version of Sun C++ will work with\n   restrict; if so, hopefully it defines __RESTRICT like Sun C does.  */\n#if defined __SUNPRO_CC && !defined __RESTRICT\n# define _Restrict\n# define __restrict__\n#endif\n\n/* Define to `unsigned int' if <sys/types.h> does not define. */\n/* #undef size_t */\n\n/* Define to `int' if <sys/types.h> does not define. */\n/* #undef ssize_t */\n\n/* Define to the type of an unsigned integer type of width exactly 16 bits if\n   such a type exists and the standard includes do not define it. */\n/* #undef uint16_t */\n\n/* Define to the type of an unsigned integer type of width exactly 8 bits if\n   such a type exists and the standard includes do not define it. */\n/* #undef uint8_t */\n\n/* Define as `fork' if `vfork' does not work. */\n/* #undef vfork */\n\n/* Define to 1 if you have the <pcre.h> header file. */\n#define HAVE_PCRE_H 1\n\n/* Define to 1 if you have the <pcre/pcre.h> header file. */\n/* #undef HAVE_PCRE_PCRE_H */\n"
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/include/sodium/version.h",
    "content": "\n#ifndef sodium_version_H\n#define sodium_version_H\n\n#include \"export.h\"\n\n#define SODIUM_VERSION_STRING \"1.0.7\"\n\n#define SODIUM_LIBRARY_VERSION_MAJOR 9\n#define SODIUM_LIBRARY_VERSION_MINOR 0\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nSODIUM_EXPORT\nconst char *sodium_version_string(void);\n\nSODIUM_EXPORT\nint         sodium_library_version_major(void);\n\nSODIUM_EXPORT\nint         sodium_library_version_minor(void);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif\n"
  },
  {
    "path": "library/shadowsocks-libev/src/main/jni/patch/pcre/pcre_chartables.c",
    "content": "/*************************************************\n*      Perl-Compatible Regular Expressions       *\n*************************************************/\n\n/* This file contains character tables that are used when no external tables\nare passed to PCRE by the application that calls it. The tables are used only\nfor characters whose code values are less than 256.\n\nThis is a default version of the tables that assumes ASCII encoding. A program\ncalled dftables (which is distributed with PCRE) can be used to build\nalternative versions of this file. This is necessary if you are running in an\nEBCDIC environment, or if you want to default to a different encoding, for\nexample ISO-8859-1. When dftables is run, it creates these tables in the\ncurrent locale. If PCRE is configured with --enable-rebuild-chartables, this\nhappens automatically.\n\nThe following #includes are present because without them gcc 4.x may remove the\narray definition from the final binary if PCRE is built into a static library\nand dead code stripping is activated. This leads to link errors. Pulling in the\nheader ensures that the array gets flagged as \"someone outside this compilation\nunit might reference this\" and so it will always be supplied to the linker. */\n\n#ifdef HAVE_CONFIG_H\n#include \"config.h\"\n#endif\n\n#include \"pcre_internal.h\"\n\nconst pcre_uint8 PRIV(default_tables)[] = {\n\n/* This table is a lower casing table. */\n\n    0,  1,  2,  3,  4,  5,  6,  7,\n    8,  9, 10, 11, 12, 13, 14, 15,\n   16, 17, 18, 19, 20, 21, 22, 23,\n   24, 25, 26, 27, 28, 29, 30, 31,\n   32, 33, 34, 35, 36, 37, 38, 39,\n   40, 41, 42, 43, 44, 45, 46, 47,\n   48, 49, 50, 51, 52, 53, 54, 55,\n   56, 57, 58, 59, 60, 61, 62, 63,\n   64, 97, 98, 99,100,101,102,103,\n  104,105,106,107,108,109,110,111,\n  112,113,114,115,116,117,118,119,\n  120,121,122, 91, 92, 93, 94, 95,\n   96, 97, 98, 99,100,101,102,103,\n  104,105,106,107,108,109,110,111,\n  112,113,114,115,116,117,118,119,\n  120,121,122,123,124,125,126,127,\n  128,129,130,131,132,133,134,135,\n  136,137,138,139,140,141,142,143,\n  144,145,146,147,148,149,150,151,\n  152,153,154,155,156,157,158,159,\n  160,161,162,163,164,165,166,167,\n  168,169,170,171,172,173,174,175,\n  176,177,178,179,180,181,182,183,\n  184,185,186,187,188,189,190,191,\n  192,193,194,195,196,197,198,199,\n  200,201,202,203,204,205,206,207,\n  208,209,210,211,212,213,214,215,\n  216,217,218,219,220,221,222,223,\n  224,225,226,227,228,229,230,231,\n  232,233,234,235,236,237,238,239,\n  240,241,242,243,244,245,246,247,\n  248,249,250,251,252,253,254,255,\n\n/* This table is a case flipping table. */\n\n    0,  1,  2,  3,  4,  5,  6,  7,\n    8,  9, 10, 11, 12, 13, 14, 15,\n   16, 17, 18, 19, 20, 21, 22, 23,\n   24, 25, 26, 27, 28, 29, 30, 31,\n   32, 33, 34, 35, 36, 37, 38, 39,\n   40, 41, 42, 43, 44, 45, 46, 47,\n   48, 49, 50, 51, 52, 53, 54, 55,\n   56, 57, 58, 59, 60, 61, 62, 63,\n   64, 97, 98, 99,100,101,102,103,\n  104,105,106,107,108,109,110,111,\n  112,113,114,115,116,117,118,119,\n  120,121,122, 91, 92, 93, 94, 95,\n   96, 65, 66, 67, 68, 69, 70, 71,\n   72, 73, 74, 75, 76, 77, 78, 79,\n   80, 81, 82, 83, 84, 85, 86, 87,\n   88, 89, 90,123,124,125,126,127,\n  128,129,130,131,132,133,134,135,\n  136,137,138,139,140,141,142,143,\n  144,145,146,147,148,149,150,151,\n  152,153,154,155,156,157,158,159,\n  160,161,162,163,164,165,166,167,\n  168,169,170,171,172,173,174,175,\n  176,177,178,179,180,181,182,183,\n  184,185,186,187,188,189,190,191,\n  192,193,194,195,196,197,198,199,\n  200,201,202,203,204,205,206,207,\n  208,209,210,211,212,213,214,215,\n  216,217,218,219,220,221,222,223,\n  224,225,226,227,228,229,230,231,\n  232,233,234,235,236,237,238,239,\n  240,241,242,243,244,245,246,247,\n  248,249,250,251,252,253,254,255,\n\n/* This table contains bit maps for various character classes. Each map is 32\nbytes long and the bits run from the least significant end of each byte. The\nclasses that have their own maps are: space, xdigit, digit, upper, lower, word,\ngraph, print, punct, and cntrl. Other classes are built from combinations. */\n\n  0x00,0x3e,0x00,0x00,0x01,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x03,\n  0x7e,0x00,0x00,0x00,0x7e,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x03,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0xfe,0xff,0xff,0x07,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0xfe,0xff,0xff,0x07,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x03,\n  0xfe,0xff,0xff,0x87,0xfe,0xff,0xff,0x07,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0xfe,0xff,0xff,0xff,\n  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,\n  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x7f,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0x00,0x00,0x00,0x00,0xfe,0xff,0x00,0xfc,\n  0x01,0x00,0x00,0xf8,0x01,0x00,0x00,0x78,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n  0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\n\n/* This table identifies various classes of character by individual bits:\n  0x01   white space character\n  0x02   letter\n  0x04   decimal digit\n  0x08   hexadecimal digit\n  0x10   alphanumeric or '_'\n  0x80   regular expression metacharacter or binary zero\n*/\n\n  0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /*   0-  7 */\n  0x00,0x01,0x01,0x01,0x01,0x01,0x00,0x00, /*   8- 15 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /*  16- 23 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /*  24- 31 */\n  0x01,0x00,0x00,0x00,0x80,0x00,0x00,0x00, /*    - '  */\n  0x80,0x80,0x80,0x80,0x00,0x00,0x80,0x00, /*  ( - /  */\n  0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c, /*  0 - 7  */\n  0x1c,0x1c,0x00,0x00,0x00,0x00,0x00,0x80, /*  8 - ?  */\n  0x00,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x12, /*  @ - G  */\n  0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12, /*  H - O  */\n  0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12, /*  P - W  */\n  0x12,0x12,0x12,0x80,0x80,0x00,0x80,0x10, /*  X - _  */\n  0x00,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x12, /*  ` - g  */\n  0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12, /*  h - o  */\n  0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12, /*  p - w  */\n  0x12,0x12,0x12,0x80,0x80,0x00,0x00,0x00, /*  x -127 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 128-135 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 136-143 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 144-151 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 152-159 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 160-167 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 168-175 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 176-183 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 184-191 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 192-199 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 200-207 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 208-215 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 216-223 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 224-231 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 232-239 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* 240-247 */\n  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};/* 248-255 */\n\n/* End of pcre_chartables.c */\n"
  },
  {
    "path": "library/stub/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n}\n\nsetupCommon()"
  },
  {
    "path": "library/stub/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.android.include\" />\n"
  },
  {
    "path": "library/stub/src/main/java/android/net/NetworkUtils.java",
    "content": "package android.net;\n\npublic class NetworkUtils {\n\n    public static boolean protectFromVpn(int socketfd) {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "library/stub/src/main/java/sun/misc/Unsafe.java",
    "content": "/*\n * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.\n * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n *\n * This code is free software; you can redistribute it and/or modify it\n * under the terms of the GNU General Public License version 2 only, as\n * published by the Free Software Foundation.  Oracle designates this\n * particular file as subject to the \"Classpath\" exception as provided\n * by Oracle in the LICENSE file that accompanied this code.\n *\n * This code is distributed in the hope that it will be useful, but WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n * version 2 for more details (a copy is included in the LICENSE file that\n * accompanied this code).\n *\n * You should have received a copy of the GNU General Public License version\n * 2 along with this work; if not, write to the Free Software Foundation,\n * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n *\n * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n * or visit www.oracle.com if you need additional information or have any\n * questions.\n */\npackage sun.misc;\n\nimport java.lang.reflect.Field;\n\n/**\n * A collection of methods for performing low-level, unsafe operations.\n * Although the class and all methods are public, use of this class is\n * limited because only trusted code can obtain instances of it.\n *\n * @author John R. Rose\n * @see #getUnsafe\n */\npublic final class Unsafe {\n    /**\n     * Traditional dalvik name.\n     */\n    private static final Unsafe THE_ONE = new Unsafe();\n    private static final Unsafe theUnsafe = THE_ONE;\n    public static final int INVALID_FIELD_OFFSET = -1;\n\n    /**\n     * This class is only privately instantiable.\n     */\n    private Unsafe() {\n    }\n\n    /**\n     * Gets the unique instance of this class. This is only allowed in\n     * very limited situations.\n     */\n    public static Unsafe getUnsafe() {\n        throw new UnsupportedOperationException();\n    }\n\n    /**\n     * Gets the raw byte offset from the start of an object's memory to\n     * the memory used to store the indicated instance field.\n     *\n     * @param field non-{@code null}; the field in question, which must be an\n     *              instance field\n     * @return the offset to the field\n     */\n    public long objectFieldOffset(Field field) {\n        throw new UnsupportedOperationException();\n    }\n\n    /**\n     * Gets the offset from the start of an array object's memory to\n     * the memory used to store its initial (zeroeth) element.\n     *\n     * @param clazz non-{@code null}; class in question; must be an array class\n     * @return the offset to the initial element\n     */\n    public int arrayBaseOffset(Class clazz) {\n        throw new UnsupportedOperationException();\n    }\n\n    /**\n     * Gets the size of each element of the given array class.\n     *\n     * @param clazz non-{@code null}; class in question; must be an array class\n     * @return &gt; 0; the size of each element of the array\n     */\n    public int arrayIndexScale(Class clazz) {\n        throw new UnsupportedOperationException();\n    }\n\n    /**\n     * Performs a compare-and-set operation on an {@code int}\n     * field within the given object.\n     *\n     * @param obj           non-{@code null}; object containing the field\n     * @param offset        offset to the field within {@code obj}\n     * @param expectedValue expected value of the field\n     * @param newValue      new value to store in the field if the contents are\n     *                      as expected\n     * @return {@code true} if the new value was in fact stored, and\n     * {@code false} if not\n     */\n    public native boolean compareAndSwapInt(Object obj, long offset,\n                                            int expectedValue, int newValue);\n\n    /**\n     * Performs a compare-and-set operation on a {@code long}\n     * field within the given object.\n     *\n     * @param obj           non-{@code null}; object containing the field\n     * @param offset        offset to the field within {@code obj}\n     * @param expectedValue expected value of the field\n     * @param newValue      new value to store in the field if the contents are\n     *                      as expected\n     * @return {@code true} if the new value was in fact stored, and\n     * {@code false} if not\n     */\n    public native boolean compareAndSwapLong(Object obj, long offset,\n                                             long expectedValue, long newValue);\n\n    /**\n     * Performs a compare-and-set operation on an {@code obj}\n     * field (that is, a reference field) within the given object.\n     *\n     * @param obj           non-{@code null}; object containing the field\n     * @param offset        offset to the field within {@code obj}\n     * @param expectedValue expected value of the field\n     * @param newValue      new value to store in the field if the contents are\n     *                      as expected\n     * @return {@code true} if the new value was in fact stored, and\n     * {@code false} if not\n     */\n    public native boolean compareAndSwapObject(Object obj, long offset,\n                                               Object expectedValue, Object newValue);\n\n    /**\n     * Gets an {@code int} field from the given object,\n     * using {@code volatile} semantics.\n     *\n     * @param obj    non-{@code null}; object containing the field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native int getIntVolatile(Object obj, long offset);\n\n    /**\n     * Stores an {@code int} field into the given object,\n     * using {@code volatile} semantics.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putIntVolatile(Object obj, long offset, int newValue);\n\n    /**\n     * Gets a {@code long} field from the given object,\n     * using {@code volatile} semantics.\n     *\n     * @param obj    non-{@code null}; object containing the field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native long getLongVolatile(Object obj, long offset);\n\n    /**\n     * Stores a {@code long} field into the given object,\n     * using {@code volatile} semantics.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putLongVolatile(Object obj, long offset, long newValue);\n\n    /**\n     * Gets an {@code obj} field from the given object,\n     * using {@code volatile} semantics.\n     *\n     * @param obj    non-{@code null}; object containing the field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native Object getObjectVolatile(Object obj, long offset);\n\n    /**\n     * Stores an {@code obj} field into the given object,\n     * using {@code volatile} semantics.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putObjectVolatile(Object obj, long offset,\n                                         Object newValue);\n\n    /**\n     * Gets an {@code int} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing int field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native int getInt(Object obj, long offset);\n\n    /**\n     * Stores an {@code int} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing int field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putInt(Object obj, long offset, int newValue);\n\n    /**\n     * Lazy set an int field.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putOrderedInt(Object obj, long offset, int newValue);\n\n    /**\n     * Gets a {@code long} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing the field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native long getLong(Object obj, long offset);\n\n    /**\n     * Stores a {@code long} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putLong(Object obj, long offset, long newValue);\n\n    /**\n     * Lazy set a long field.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putOrderedLong(Object obj, long offset, long newValue);\n\n    /**\n     * Gets an {@code obj} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing the field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native Object getObject(Object obj, long offset);\n\n    /**\n     * Stores an {@code obj} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putObject(Object obj, long offset, Object newValue);\n\n    /**\n     * Lazy set an object field.\n     *\n     * @param obj      non-{@code null}; object containing the field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putOrderedObject(Object obj, long offset,\n                                        Object newValue);\n\n    /**\n     * Gets a {@code boolean} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing boolean field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native boolean getBoolean(Object obj, long offset);\n\n    /**\n     * Stores a {@code boolean} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing boolean field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putBoolean(Object obj, long offset, boolean newValue);\n\n    /**\n     * Gets a {@code byte} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing byte field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native byte getByte(Object obj, long offset);\n\n    /**\n     * Stores a {@code byte} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing byte field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putByte(Object obj, long offset, byte newValue);\n\n    /**\n     * Gets a {@code char} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing char field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native char getChar(Object obj, long offset);\n\n    /**\n     * Stores a {@code char} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing char field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putChar(Object obj, long offset, char newValue);\n\n    /**\n     * Gets a {@code short} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing short field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native short getShort(Object obj, long offset);\n\n    /**\n     * Stores a {@code short} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing short field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putShort(Object obj, long offset, short newValue);\n\n    /**\n     * Gets a {@code float} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing float field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native float getFloat(Object obj, long offset);\n\n    /**\n     * Stores a {@code float} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing float field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putFloat(Object obj, long offset, float newValue);\n\n    /**\n     * Gets a {@code double} field from the given object.\n     *\n     * @param obj    non-{@code null}; object containing double field\n     * @param offset offset to the field within {@code obj}\n     * @return the retrieved value\n     */\n    public native double getDouble(Object obj, long offset);\n\n    /**\n     * Stores a {@code double} field into the given object.\n     *\n     * @param obj      non-{@code null}; object containing double field\n     * @param offset   offset to the field within {@code obj}\n     * @param newValue the value to store\n     */\n    public native void putDouble(Object obj, long offset, double newValue);\n\n    /**\n     * Parks the calling thread for the specified amount of time,\n     * unless the \"permit\" for the thread is already available (due to\n     * a previous call to {@link #unpark}. This method may also return\n     * spuriously (that is, without the thread being told to unpark\n     * and without the indicated amount of time elapsing).\n     *\n     * <p>See {@link java.util.concurrent.locks.LockSupport} for more\n     * in-depth information of the behavior of this method.</p>\n     *\n     * @param absolute whether the given time value is absolute\n     *                 milliseconds-since-the-epoch ({@code true}) or relative\n     *                 nanoseconds-from-now ({@code false})\n     * @param time     the (absolute millis or relative nanos) time value\n     */\n    public native void park(boolean absolute, long time);\n\n    /**\n     * Unparks the given object, which must be a {@link Thread}.\n     *\n     * <p>See {@link java.util.concurrent.locks.LockSupport} for more\n     * in-depth information of the behavior of this method.</p>\n     *\n     * @param obj non-{@code null}; the object to unpark\n     */\n    public native void unpark(Object obj);\n\n    /**\n     * Allocates an instance of the given class without running the constructor.\n     * The class' <clinit> will be run, if necessary.\n     */\n    public native Object allocateInstance(Class<?> c);\n\n    /**\n     * Gets the size of the address value, in bytes.\n     *\n     * @return the size of the address, in bytes\n     */\n    public native int addressSize();\n\n    /**\n     * Gets the size of the memory page, in bytes.\n     *\n     * @return the size of the page\n     */\n    public native int pageSize();\n\n    /**\n     * Allocates a memory block of size {@code bytes}.\n     *\n     * @param bytes size of the memory block\n     * @return address of the allocated memory\n     */\n    public native long allocateMemory(long bytes);\n\n    /**\n     * Frees previously allocated memory at given address.\n     *\n     * @param address address of the freed memory\n     */\n    public native void freeMemory(long address);\n\n    /**\n     * Fills given memory block with a given value.\n     *\n     * @param address address of the memoory block\n     * @param bytes   length of the memory block, in bytes\n     * @param value   fills memory with this value\n     */\n    public native void setMemory(long address, long bytes, byte value);\n\n    /**\n     * Gets {@code byte} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code byte} value\n     */\n    public native byte getByte(long address);\n\n    /**\n     * Stores a {@code byte} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putByte(long address, byte x);\n\n    /**\n     * Gets {@code short} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code short} value\n     */\n    public native short getShort(long address);\n\n    /**\n     * Stores a {@code short} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putShort(long address, short x);\n\n    /**\n     * Gets {@code char} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code char} value\n     */\n    public native char getChar(long address);\n\n    /**\n     * Stores a {@code char} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putChar(long address, char x);\n\n    /**\n     * Gets {@code int} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code int} value\n     */\n    public native int getInt(long address);\n\n    /**\n     * Stores a {@code int} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putInt(long address, int x);\n\n    /**\n     * Gets {@code long} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code long} value\n     */\n    public native long getLong(long address);\n\n    /**\n     * Stores a {@code long} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putLong(long address, long x);\n\n    /**\n     * Gets {@code long} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code long} value\n     */\n    public native float getFloat(long address);\n\n    /**\n     * Stores a {@code float} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putFloat(long address, float x);\n\n    /**\n     * Gets {@code double} from given address in memory.\n     *\n     * @param address address in memory\n     * @return {@code double} value\n     */\n    public native double getDouble(long address);\n\n    /**\n     * Stores a {@code double} into the given memory address.\n     *\n     * @param address  address in memory where to store the value\n     * @param newValue the value to store\n     */\n    public native void putDouble(long address, double x);\n\n    /**\n     * Copies given memory block to a primitive array.\n     *\n     * @param srcAddr   address to copy memory from\n     * @param dst       address to copy memory to\n     * @param dstOffset offset in {@code dst}\n     * @param bytes     number of bytes to copy\n     */\n    public native void copyMemoryToPrimitiveArray(long srcAddr,\n                                                  Object dst, long dstOffset, long bytes);\n\n    /**\n     * Treat given primitive array as a continuous memory block and\n     * copy it to given memory address.\n     *\n     * @param src       primitive array to copy data from\n     * @param srcOffset offset in {@code src} to copy from\n     * @param dstAddr   memory address to copy data to\n     * @param bytes     number of bytes to copy\n     */\n    public native void copyMemoryFromPrimitiveArray(Object src, long srcOffset,\n                                                    long dstAddr, long bytes);\n\n    /**\n     * Sets all bytes in a given block of memory to a copy of another block.\n     *\n     * @param srcAddr address of the source memory to be copied from\n     * @param dstAddr address of the destination memory to copy to\n     * @param bytes   number of bytes to copy\n     */\n    public native void copyMemory(long srcAddr, long dstAddr, long bytes);\n    // The following contain CAS-based Java implementations used on\n    // platforms not supporting native instructions\n\n    /**\n     * Atomically adds the given value to the current value of a field\n     * or array element within the given object {@code o}\n     * at the given {@code offset}.\n     *\n     * @param o      object/array to update the field/element in\n     * @param offset field/element offset\n     * @param delta  the value to add\n     * @return the previous value\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public final int getAndAddInt(Object o, long offset, int delta) {\n        int v;\n        do {\n            v = getIntVolatile(o, offset);\n        } while (!compareAndSwapInt(o, offset, v, v + delta));\n        return v;\n    }\n\n    /**\n     * Atomically adds the given value to the current value of a field\n     * or array element within the given object {@code o}\n     * at the given {@code offset}.\n     *\n     * @param o      object/array to update the field/element in\n     * @param offset field/element offset\n     * @param delta  the value to add\n     * @return the previous value\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public final long getAndAddLong(Object o, long offset, long delta) {\n        long v;\n        do {\n            v = getLongVolatile(o, offset);\n        } while (!compareAndSwapLong(o, offset, v, v + delta));\n        return v;\n    }\n\n    /**\n     * Atomically exchanges the given value with the current value of\n     * a field or array element within the given object {@code o}\n     * at the given {@code offset}.\n     *\n     * @param o        object/array to update the field/element in\n     * @param offset   field/element offset\n     * @param newValue new value\n     * @return the previous value\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public final int getAndSetInt(Object o, long offset, int newValue) {\n        int v;\n        do {\n            v = getIntVolatile(o, offset);\n        } while (!compareAndSwapInt(o, offset, v, newValue));\n        return v;\n    }\n\n    /**\n     * Atomically exchanges the given value with the current value of\n     * a field or array element within the given object {@code o}\n     * at the given {@code offset}.\n     *\n     * @param o        object/array to update the field/element in\n     * @param offset   field/element offset\n     * @param newValue new value\n     * @return the previous value\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public final long getAndSetLong(Object o, long offset, long newValue) {\n        long v;\n        do {\n            v = getLongVolatile(o, offset);\n        } while (!compareAndSwapLong(o, offset, v, newValue));\n        return v;\n    }\n\n    /**\n     * Atomically exchanges the given reference value with the current\n     * reference value of a field or array element within the given\n     * object {@code o} at the given {@code offset}.\n     *\n     * @param o        object/array to update the field/element in\n     * @param offset   field/element offset\n     * @param newValue new value\n     * @return the previous value\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public final Object getAndSetObject(Object o, long offset, Object newValue) {\n        Object v;\n        do {\n            v = getObjectVolatile(o, offset);\n        } while (!compareAndSwapObject(o, offset, v, newValue));\n        return v;\n    }\n\n    /**\n     * Ensures that loads before the fence will not be reordered with loads and\n     * stores after the fence; a \"LoadLoad plus LoadStore barrier\".\n     * <p>\n     * Corresponds to C11 atomic_thread_fence(memory_order_acquire)\n     * (an \"acquire fence\").\n     * <p>\n     * A pure LoadLoad fence is not provided, since the addition of LoadStore\n     * is almost always desired, and most current hardware instructions that\n     * provide a LoadLoad barrier also provide a LoadStore barrier for free.\n     *\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public native void loadFence();\n\n    /**\n     * Ensures that loads and stores before the fence will not be reordered with\n     * stores after the fence; a \"StoreStore plus LoadStore barrier\".\n     * <p>\n     * Corresponds to C11 atomic_thread_fence(memory_order_release)\n     * (a \"release fence\").\n     * <p>\n     * A pure StoreStore fence is not provided, since the addition of LoadStore\n     * is almost always desired, and most current hardware instructions that\n     * provide a StoreStore barrier also provide a LoadStore barrier for free.\n     *\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public native void storeFence();\n\n    /**\n     * Ensures that loads and stores before the fence will not be reordered\n     * with loads and stores after the fence.  Implies the effects of both\n     * loadFence() and storeFence(), and in addition, the effect of a StoreLoad\n     * barrier.\n     * <p>\n     * Corresponds to C11 atomic_thread_fence(memory_order_seq_cst).\n     *\n     * @since 1.8\n     */\n    // @HotSpotIntrinsicCandidate\n    public native void fullFence();\n}"
  },
  {
    "path": "lint.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n    <issue id=\"UnusedAttribute\" severity=\"ignore\" />\n    <issue id=\"UnknownNullness\" severity=\"ignore\" />\n    <issue id=\"DiscouragedPrivateApi\" severity=\"ignore\" />\n    <issue id=\"Typos\" severity=\"ignore\" />\n    <issue id=\"RtlSymmetry\" severity=\"ignore\" />\n    <issue id=\"ContentDescription\" severity=\"ignore\" />\n    <issue id=\"UnusedResources\" severity=\"ignore\" />\n    <issue id=\"VectorRaster\" severity=\"ignore\" />\n    <issue id=\"PrivateResource\" severity=\"ignore\" />\n\n    <issue id=\"Untranslatable\" severity=\"ignore\" />\n    <issue id=\"PluralsCandidate\" severity=\"ignore\" />\n    <issue id=\"TypographyQuotes\" severity=\"ignore\" />\n    <issue id=\"DuplicateStrings\" severity=\"ignore\" />\n    <issue id=\"ImpliedQuantity\" severity=\"ignore\" />\n\n    <issue id=\"SelectableText\" severity=\"ignore\" />\n    <issue id=\"InsecureBaseConfiguration\" severity=\"ignore\" />\n    <issue id=\"NotifyDataSetChanged\" severity=\"ignore\" />\n    <issue id=\"UselessParent\" severity=\"ignore\" />\n    <issue id=\"SyntheticAccessor\" severity=\"ignore\" />\n    <issue id=\"UselessLeaf\" severity=\"ignore\" />\n    <issue id=\"UnusedIds\" severity=\"ignore\" />\n    <issue id=\"ConvertToWebp\" severity=\"ignore\" />\n    <issue id=\"SetTextI18n\" severity=\"ignore\" />\n    <issue id=\"HardcodedText\" severity=\"ignore\" />\n    <issue id=\"RtlHardcoded\" severity=\"ignore\" />\n    <issue id=\"MissingTranslation\" severity=\"ignore\" />\n    <issue id=\"ExtraTranslation\" severity=\"ignore\" />\n    <issue id=\"RestrictedApi\" severity=\"ignore\" />\n\n    <issue id=\"InvalidPackage\" severity=\"ignore\" />\n    <issue id=\"TrustAllX509TrustManager\" severity=\"ignore\" />\n</lint>\n"
  },
  {
    "path": "plugin/api/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    kotlin(\"android\")\n    id(\"kotlin-parcelize\")\n}\n\nsetupKotlinCommon()"
  },
  {
    "path": "plugin/api/proguard-rules.pro",
    "content": "-repackageclasses ''\n-allowaccessmodification"
  },
  {
    "path": "plugin/api/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"io.nekohasekai.sagernet.plugin\"\n    android:installLocation=\"internalOnly\" />"
  },
  {
    "path": "plugin/api/src/main/java/io/nekohasekai/sagernet/plugin/NativePluginProvider.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.content.ContentProvider\nimport android.content.ContentValues\nimport android.database.Cursor\nimport android.database.MatrixCursor\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.ParcelFileDescriptor\n\nabstract class NativePluginProvider : ContentProvider() {\n    override fun getType(uri: Uri): String? = \"application/x-elf\"\n\n    override fun onCreate(): Boolean = true\n\n    /**\n     * Provide all files needed for native plugin.\n     *\n     * @param provider A helper object to use to add files.\n     */\n    protected abstract fun populateFiles(provider: PathProvider)\n\n    override fun query(\n        uri: Uri,\n        projection: Array<out String>?,\n        selection: String?,\n        selectionArgs: Array<out String>?,\n        sortOrder: String?,\n    ): Cursor? {\n        check(selection == null && selectionArgs == null && sortOrder == null)\n        val result = MatrixCursor(projection)\n        populateFiles(PathProvider(uri, result))\n        return result\n    }\n\n    /**\n     * Returns executable entry absolute path.\n     * This is used for fast mode initialization where ss-local launches your native binary at the path given directly.\n     * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:\n     *  - android:installLocation=\"internalOnly\" for <manifest>\n     *  - android:extractNativeLibs=\"true\" for <application>\n     *\n     * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the\n     * default behavior.\n     *\n     * @return Absolute path for executable entry.\n     */\n    open fun getExecutable(): String = throw UnsupportedOperationException()\n\n    abstract fun openFile(uri: Uri): ParcelFileDescriptor\n    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {\n        check(mode == \"r\")\n        return openFile(uri)\n    }\n\n    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {\n        PluginContract.METHOD_GET_EXECUTABLE -> {\n            Bundle().apply {\n                putString(PluginContract.EXTRA_ENTRY, getExecutable())\n            }\n        }\n        else -> super.call(method, arg, extras)\n    }\n\n    // Methods that should not be used\n    override fun insert(uri: Uri, values: ContentValues?): Uri? =\n        throw UnsupportedOperationException()\n\n    override fun update(\n        uri: Uri,\n        values: ContentValues?,\n        selection: String?,\n        selectionArgs: Array<out String>?,\n    ): Int =\n        throw UnsupportedOperationException()\n\n    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =\n        throw UnsupportedOperationException()\n}\n"
  },
  {
    "path": "plugin/api/src/main/java/io/nekohasekai/sagernet/plugin/PathProvider.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nimport android.database.MatrixCursor\nimport android.net.Uri\nimport java.io.File\n\n/**\n * Helper class to provide relative paths of files to copy.\n */\nclass PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {\n    private val basePath = baseUri.path?.trim('/') ?: \"\"\n\n    fun addPath(path: String, mode: Int = 0b110100100): PathProvider {\n        val trimmed = path.trim('/')\n        if (trimmed.startsWith(basePath)) cursor.newRow()\n                .add(PluginContract.COLUMN_PATH, trimmed)\n                .add(PluginContract.COLUMN_MODE, mode)\n        return this\n    }\n    fun addTo(file: File, to: String = \"\", mode: Int = 0b110100100): PathProvider {\n        var sub = to + file.name\n        if (basePath.startsWith(sub)) if (file.isDirectory) {\n            sub += '/'\n            file.listFiles()!!.forEach { addTo(it, sub, mode) }\n        } else addPath(sub, mode)\n        return this\n    }\n    fun addAt(file: File, at: String = \"\", mode: Int = 0b110100100): PathProvider {\n        if (basePath.startsWith(at)) {\n            if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode)\n        }\n        return this\n    }\n}\n"
  },
  {
    "path": "plugin/api/src/main/java/io/nekohasekai/sagernet/plugin/PluginContract.kt",
    "content": "/******************************************************************************\n *                                                                            *\n * Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>             *\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 <http://www.gnu.org/licenses/>.       *\n *                                                                            *\n ******************************************************************************/\n\npackage io.nekohasekai.sagernet.plugin\n\nobject PluginContract {\n\n    const val ACTION_NATIVE_PLUGIN = \"io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN\"\n    const val EXTRA_ENTRY = \"io.nekohasekai.sagernet.plugin.EXTRA_ENTRY\"\n    const val METADATA_KEY_ID = \"io.nekohasekai.sagernet.plugin.id\"\n    const val METADATA_KEY_EXECUTABLE_PATH = \"io.nekohasekai.sagernet.plguin.executable_path\"\n    const val METHOD_GET_EXECUTABLE = \"sagernet:getExecutable\"\n\n    const val COLUMN_PATH = \"path\"\n    const val COLUMN_MODE = \"mode\"\n    const val SCHEME = \"plugin\"\n    const val AUTHORITY = \"io.nekohasekai.sagernet\"\n}\n"
  },
  {
    "path": "repositories.gradle.kts",
    "content": "rootProject.extra.apply {\n    set(\"androidPluginVersion\", \"7.0.3\")\n    set(\"kotlinVersion\", \"1.5.31\")\n}\n\nrepositories {\n    google()\n    mavenCentral()\n    gradlePluginPortal()\n    maven(url = \"https://jitpack.io\")\n}"
  },
  {
    "path": "run",
    "content": "#!/bin/bash\n\nEXEC=\"\"\nTARGET=\"bin\"\ndeclare -A PARAMS\ndeclare -i INDEX=0\n\nfor e in $@; do\n  TARGET=\"$TARGET/$e\"\n  PARAMS[$INDEX]=$e\n  INDEX=$INDEX+1\n  shift\n  if [ -x \"${TARGET}.sh\" ]; then\n    EXEC=\"${TARGET}.sh\"\n    PARAMS=()\n    INDEX=0\n  fi\ndone\n\necho \">> $EXEC\"\nexec \"$EXEC\" $PARAMS\n"
  },
  {
    "path": "sager.properties",
    "content": "PACKAGE_NAME=io.nekohasekai.anXray\nVERSION_NAME=0.4-rc06\nVERSION_CODE=35\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "include(\":library:stub\")\ninclude(\":library:include\")\n//include(\":library:proto\")\n//include(\":library:proto-stub\")\n\ninclude(\":library:shadowsocks\")\ninclude(\":library:shadowsocks-libev\")\n\ninclude(\":plugin:api\")\n\ninclude(\":external:preferencex:preferencex\")\ninclude(\":external:preferencex:preferencex-simplemenu\")\ninclude(\":external:preferencex:flexbox\")\ninclude(\":external:preferencex:colorpicker\")\ninclude(\":external:preferencex:preferencex-colorpicker\")\n\nincludeBuild(\"external/editorkit\") {\n    name = \"editorkit\"\n    dependencySubstitution {\n        substitute(module(\"editorkit:editorkit:2.0.0\")).using(project(\":editorkit\"))\n        substitute(module(\"editorkit:feature-editor:2.0.0\")).using(project(\":features:feature-editor\"))\n        substitute(module(\"editorkit:language-json:2.0.0\")).using(project(\":languages:language-json\"))\n    }\n}\n\nincludeBuild(\"external/termux-view\") {\n    name = \"termux-view\"\n    dependencySubstitution {\n        substitute(module(\"termux:terminal-view:1.0\")).using(project(\":terminal-view\"))\n    }\n}\n\ninclude(\":app\")\nrootProject.name = \"AnXray\""
  }
]