[
  {
    "path": ".gitattributes",
    "content": "# Set the default behavior, in case people don't have core.autocrlf set.\n* text=auto eol=lf\n\n# Declare files that will always have CRLF line endings on checkout.\n*.cmd text eol=crlf\n*.bat text eol=crlf\n\n# Denote all files that are truly binary and should not be modified.\n*.so binary\n*.dex binary\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report/反馈 Bug\ndescription: Report errors or unexpected behavior./反馈错误或异常行为。\nlabels: [bug]\ntitle: \"[Bug] \"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for reporting issues of LSPatch!\n\n        To make it easier for us to help you please enter detailed information below.\n        \n        感谢给 LSPatch 汇报问题！\n        为了使我们更好地帮助你，请提供以下信息。\n        为了防止重复汇报，标题请务必使用英文。\n  - type: textarea\n    attributes:\n      label: Steps to reproduce/复现步骤\n      value: |\n        1. \n        1. \n        1. \n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Expected behaviour/预期行为\n      placeholder: Tell us what should happen/正常情况下应该发生什么\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Actual behaviour/实际行为\n      placeholder: Tell us what happens instead/实际上发生了什么\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Xposed Module List/Xposed 模块列表\n      render: Shell\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: LSPatch version/LSPatch 版本\n      description:  Don't use 'latest'. Specify actual version with 4 digits, otherwise your issue will be closed./不要填用“最新版”。给出四位版本号，不然 issue 会被关闭。\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Android version/Android 版本\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Shizuku version/Shizuku 版本\n      description: If you are not using Shizuku mode, input `N/A` instead./如果未使用 Shizuku 模式，请键入 `N/A`。\n    validations:\n      required: true\n  - type: checkboxes\n    id: latest\n    attributes:\n      label: Version requirement/版本要求\n      options:\n        - label: I am using latest debug CI version of LSPatch and enable verbose log/我正在使用最新 CI 调试版本且启用详细日志\n          required: true\n  - type: textarea\n    attributes:\n      label: Apk file/Apk 文件\n      description: The apk file if patching failed / 修复失败的 apk 文件（如有）\n      placeholder: Upload apks by clicking the bar on the bottom. /点击文本框底栏上传安装包文件。\n  - type: textarea\n    attributes:\n      label: Logs/日志\n      description: For usage issues, please provide the log zip saved from manager; for activation issues, please provide [bugreport](https://developer.android.com/studio/debug/bug-report). Without log, the issue will be closed. /使用问题请提供从管理器保存的日志压缩包；激活问题请提供 [bugreport](https://developer.android.google.cn/studio/debug/bug-report?hl=zh-cn) 日志。无日志提交会被关闭。\n      value: |\n        <details>\n        \n        ```\n        # Replace this line with the log / 将此行用日志替换\n        ```\n        </details>\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question/提问\n    url: https://github.com/LSPosed/LSPatch/discussions/new?category=Q-A\n    about: Please ask and answer questions here./如果有任何疑问请在这里提问\n  - name: Official Telegram Channel/官方 Telegram 频道\n    url: https://t.me/LSPosed\n    about: Subscribe for notifications and releases/可以订阅通知和发行版\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "---\nname: Feature request/新特性请求\ndescription: Suggest an idea./提出建议\nlabels: [enhancement]\ntitle: \"[Feature Request] \"\nbody:\n  - type: textarea\n    attributes:\n      label: Is your feature request related to a problem?/你的请求是否与某个问题相关？\n      placeholder: A clear and concise description of what the problem is./请清晰准确表述该问题。\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like/描述你想要的解决方案\n      placeholder: A clear and concise description of what you want to happen./请清晰准确描述新特性的预期行为\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Additional context/其他信息\n      placeholder: Add any other context or screenshots about the feature request here./其他关于新特性的信息或者截图\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/workflows/crowdin.yml",
    "content": "name: Crowdin Action\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ master ]\n    paths:\n    - manager/src/main/res/values/strings.xml\n\njobs:\n  synchronize-with-crowdin:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v3\n\n    - name: crowdin action\n      uses: crowdin/github-action@master\n      with:\n        upload_translations: false\n        download_translations: false\n        upload_sources: true\n        config: 'crowdin.yml'\n        crowdin_branch_name: master\n      env:\n        CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}\n        CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Android CI\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ master ]\n  pull_request:\n\njobs:\n  build:\n    name: Build on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ ubuntu-latest ]\n    env:\n      CCACHE_COMPILERCHECK: '%compiler% -dumpmachine; %compiler% -dumpversion'\n      CCACHE_NOHASHDIR: 'true'\n      CCACHE_HARDLINK: 'true'\n      CCACHE_BASEDIR: '${{ github.workspace }}'\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v3\n      with:\n        submodules: 'recursive'\n        fetch-depth: 0\n\n    - name: Write key\n      if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'\n      run: |\n        if [ ! -z \"${{ secrets.KEY_STORE }}\" ]; then\n          echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties\n          echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties\n          echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties\n          echo androidStoreFile='key.jks' >> gradle.properties\n          echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks\n        fi\n\n    - name: Checkout libxposed/api\n      uses: actions/checkout@v3\n      with:\n        repository: libxposed/api\n        path: libxposed/api\n\n    - name: Checkout libxposed/service\n      uses: actions/checkout@v3\n      with:\n        repository: libxposed/service\n        path: libxposed/service\n\n    - name: Setup Java\n      uses: actions/setup-java@v3\n      with:\n        java-version: '17'\n        distribution: 'temurin'\n\n    - name: Setup Gradle\n      uses: gradle/gradle-build-action@v2\n      with:\n        gradle-home-cache-cleanup: true\n\n    - name: Set up ccache\n      uses: hendrikmuhs/ccache-action@v1.2\n      with:\n        max-size: 2G\n        key: ${{ runner.os }}\n        restore-keys: ${{ runner.os }}\n        save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}\n\n    - name: Build dependencies\n      working-directory: libxposed\n      run: |\n        cd api\n        echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties\n        ./gradlew :api:publishApiPublicationToMavenLocal\n        cd ..\n        cd service\n        echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties\n        ./gradlew :interface:publishInterfacePublicationToMavenLocal\n\n    - name: Build with Gradle\n      run: |\n        echo 'org.gradle.parallel=true' >> gradle.properties\n        echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties\n        echo 'android.native.buildOutput=verbose' >> gradle.properties\n        ./gradlew buildAll\n\n    - name: Upload Debug artifact\n      uses: actions/upload-artifact@v3\n      with:\n        name: lspatch-debug\n        path: out/debug/*\n\n    - name: Upload Release artifact\n      uses: actions/upload-artifact@v3\n      with:\n        name: lspatch-release\n        path: out/release/*\n\n    - name: Upload mappings\n      uses: actions/upload-artifact@v3\n      with:\n        name: mappings\n        path: |\n          patch-loader/build/outputs/mapping\n          manager/build/outputs/mapping\n\n    - name: Upload symbols\n      uses: actions/upload-artifact@v3\n      with:\n        name: symbols\n        path: |\n          patch-loader/build/symbols\n\n    - name: Post to channel\n      if: ${{ github.event_name != 'pull_request' && success() && github.ref == 'refs/heads/master' }}\n      env:\n        CHANNEL_ID: ${{ secrets.CHANNEL_ID }}\n        DISCUSSION_ID: ${{ secrets.DISCUSSION_ID }}\n        TOPIC_ID: ${{ secrets.TOPIC_ID }}\n        BOT_TOKEN: ${{ secrets.BOT_TOKEN }}\n        COMMIT_MESSAGE: ${{ github.event.head_commit.message }}\n        COMMIT_URL: ${{ github.event.head_commit.url }}\n      run: |\n        if [ ! -z \"${{ secrets.BOT_TOKEN }}\" ]; then\n          export jarRelease=$(find out/release -name \"*.jar\")\n          export managerRelease=$(find out/release -name \"*.apk\")\n          export jarDebug=$(find out/debug -name \"*.jar\")\n          export managerDebug=$(find out/debug -name \"*.apk\")\n          ESCAPED=`python3 -c 'import json,os,urllib.parse; msg = json.dumps(os.environ[\"COMMIT_MESSAGE\"]); print(urllib.parse.quote(msg if len(msg) <= 1024 else json.dumps(os.environ[\"COMMIT_URL\"])))'`\n          curl -v \"https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerDebug%22%2C%22caption%22:${ESCAPED}%7D%5D\" -F jarRelease=\"@$jarRelease\" -F managerRelease=\"@$managerRelease\" -F jarDebug=\"@$jarDebug\" -F managerDebug=\"@$managerDebug\"\n          # curl -v \"https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${DISCUSSION_ID}&message_thread_id=${TOPIC_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerRelease%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FjarDebug%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2FmanagerDebug%22%2C%22caption%22:${ESCAPED}%7D%5D\" -F jarRelease=\"@$jarRelease\" -F managerRelease=\"@$managerRelease\" -F jarDebug=\"@$jarDebug\" -F managerDebug=\"@$managerDebug\"\n        fi\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\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\n/build\n/captures\n.externalNativeBuild\n.cxx\n/out\n/.idea\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"apksigner\"]\n\tpath = apksigner\n\turl = https://android.googlesource.com/platform/tools/apksig.git\n\tbranch = android10-release\n[submodule \"core\"]\n\tpath = core\n\turl = https://github.com/LSPosed/LSPosed.git\n\tbranch = master\n[submodule \"patch/libs/manifest-editor\"]\n\tpath = patch/libs/manifest-editor\n\turl = https://github.com/WindySha/ManifestEditor.git\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# LSPatch Framework\n\n[![Build](https://img.shields.io/github/actions/workflow/status/LSPosed/LSPatch/main.yml?branch=master&logo=github&label=Build&event=push)](https://github.com/LSPosed/LSPatch/actions/workflows/main.yml?query=event%3Apush+is%3Acompleted+branch%3Amaster) [![Crowdin](https://img.shields.io/badge/Localization-Crowdin-blueviolet?logo=Crowdin)](https://lsposed.crowdin.com/lspatch) [![Download](https://img.shields.io/github/v/release/LSPosed/LSPatch?color=orange&logoColor=orange&label=Download&logo=DocuSign)](https://github.com/LSPosed/LSPatch/releases/latest) [![Total](https://shields.io/github/downloads/LSPosed/LSPatch/total?logo=Bookmeter&label=Counts&logoColor=yellow&color=yellow)](https://github.com/LSPosed/LSPatch/releases)\n\n## Introduction \n\nRootless implementation of LSPosed framework, integrating Xposed API by inserting dex and so into the target APK.\n\n## Supported Versions\n\n- Min: Android 9\n- Max: In theory, same with [LSPosed](https://github.com/LSPosed/LSPosed#supported-versions)\n\n## Download\n\nFor stable releases, please go to [Github Releases page](https://github.com/LSPosed/LSPatch/releases)  \nFor canary build, please check [Github Actions](https://github.com/LSPosed/LSPatch/actions)  \nNote: debug builds are only available in Github Actions  \n\n## Usage\n\n+ Through jar\n1. Download `lspatch.jar`\n1. Run `java -jar lspatch.jar`\n\n+ Through manager\n1. Download and install `manager.apk` on an Android device\n1. Follow the instructions of the manager app\n\n## Translation Contributing\n\nYou can contribute translation [here](https://lsposed.crowdin.com/lspatch).\n\n## Credits\n\n- [LSPosed](https://github.com/LSPosed/LSPosed): Core framework\n- [Xpatch](https://github.com/WindySha/Xpatch): Fork source\n- [Apkzlib](https://android.googlesource.com/platform/tools/apkzlib): Repacking tool\n\n## License\n\nLSPatch is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html).\n"
  },
  {
    "path": "apkzlib/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "apkzlib/build.gradle.kts",
    "content": "val androidSourceCompatibility: JavaVersion by rootProject.extra\nval androidTargetCompatibility: JavaVersion by rootProject.extra\n\nplugins {\n    id(\"java-library\")\n}\n\njava {\n    sourceCompatibility = androidSourceCompatibility\n    targetCompatibility = androidTargetCompatibility\n}\n\ndependencies {\n    implementation(\"com.google.code.findbugs:jsr305:3.0.2\")\n    implementation(\"org.bouncycastle:bcpkix-jdk15on:1.70\")\n    implementation(\"org.bouncycastle:bcprov-jdk15on:1.70\")\n    api(\"com.google.guava:guava:32.0.1-jre\")\n    api(\"com.android.tools.build:apksig:8.0.2\")\n    compileOnlyApi(\"com.google.auto.value:auto-value-annotations:1.10.1\")\n    annotationProcessor(\"com.google.auto.value:auto-value:1.10.1\")\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/AbstractCloseableByteSourceFromOutputStreamBuilder.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.base.Preconditions;\nimport java.io.IOException;\n\n/**\n * Abstract implementation of a {@link CloseableByteSourceFromOutputStreamBuilder} that simplifies\n * the implementation of concrete instances. It implements the state machine implied by the\n * interface contract and requires subclasses to implement two methods:\n * {@link #doWrite(byte[], int, int)} -- that actually does writing and {@link #doBuild()} that\n * builds the {@link CloseableByteSource].\n */\nabstract class AbstractCloseableByteSourceFromOutputStreamBuilder\n    extends CloseableByteSourceFromOutputStreamBuilder {\n\n  /**\n   * Array that allows {@link #write(int)} to delegate to {@link #write(byte[], int, int)} without\n   * having to create an array for each invocation.\n   */\n  private final byte[] tempByte;\n\n  /**\n   * Has the builder been closed? If it has, then {@link #build()} may be called, but none of the\n   * writing methods can.\n   */\n  private boolean closed;\n\n  /**\n   * Has the builder been built? If this is {@code true} then {@link #closed} is also {@code true}.\n   */\n  private boolean built;\n\n  /** Creates a new builder. */\n  AbstractCloseableByteSourceFromOutputStreamBuilder() {\n    tempByte = new byte[1];\n    closed = false;\n    built = false;\n  }\n\n  @Override\n  public void write(byte[] b, int off, int len) throws IOException {\n    Preconditions.checkState(!closed);\n    doWrite(b, off, len);\n  }\n\n  @Override\n  public void write(int b) throws IOException {\n    tempByte[0] = (byte) b;\n    write(tempByte, 0, 1);\n  }\n\n  @Override\n  public void close() throws IOException {\n    closed = true;\n  }\n\n  @Override\n  public CloseableByteSource build() throws IOException {\n    Preconditions.checkState(!built);\n    closed = true;\n    built = true;\n\n    return doBuild();\n  }\n\n  /**\n   * Same as {@link #write(byte[], int, int)}, but with the guarantee that the source has not been\n   * built and the builder is still open.\n   *\n   * @param b see {@link #write(byte[], int, int)}\n   * @param off see {@link #write(byte[], int, int)}\n   * @param len see {@link #write(byte[], int, int)}\n   * @throws IOException see {@link #write(byte[], int, int)}\n   */\n  protected abstract void doWrite(byte[] b, int off, int len) throws IOException;\n\n  /**\n   * Builds the {@link CloseableByteSource} from the written data. This method is at most invoked\n   * once.\n   *\n   * @return the new source that will contain all data written to the builder so far\n   * @throws IOException failed to create the byte source\n   */\n  protected abstract CloseableByteSource doBuild() throws IOException;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ByteStorage.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.io.ByteSource;\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Interface for a storage that will temporarily save bytes. There are several factory methods to\n * create byte sources from several inputs, all of which may be discarded after the byte source has\n * been created. The data is saved in the storage and will be kept until the byte source is closed.\n */\npublic interface ByteStorage extends Closeable {\n  /**\n   * Creates a new byte source by fully reading an input stream.\n   *\n   * @param stream the input stream\n   * @return a byte source containing the cached data from the given stream\n   * @throws IOException failed to read the stream\n   */\n  CloseableByteSource fromStream(InputStream stream) throws IOException;\n\n  /**\n   * Creates a builder that is an output stream and can create a byte source.\n   *\n   * @return a builder where data can be written to and a {@link CloseableByteSource} can eventually\n   *     be obtained from\n   * @throws IOException failed to create the builder; this may happen if the builder require some\n   *     preparation such as temporary storage allocation that may fail\n   */\n  CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException;\n\n  /**\n   * Creates a new byte source from another byte source.\n   *\n   * @param source the byte source to copy data from\n   * @return the tracked byte source\n   * @throws IOException failed to read data from the byte source\n   */\n  CloseableByteSource fromSource(ByteSource source) throws IOException;\n\n  /**\n   * Obtains the number of bytes currently used.\n   *\n   * @return the number of bytes\n   */\n  long getBytesUsed();\n\n  /**\n   * Obtains the maximum number of bytes ever used by this tracker.\n   *\n   * @return the number of bytes\n   */\n  long getMaxBytesUsed();\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ByteStorageFactory.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport java.io.IOException;\n\n/** Factory that creates {@link ByteStorage}. */\npublic interface ByteStorageFactory {\n\n  /**\n   * Creates a new storage.\n   *\n   * @return a storage that should be closed when no longer used.\n   */\n  ByteStorage create() throws IOException;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ChunkBasedByteStorage.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.io.ByteSource;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.annotation.Nullable;\n\n/**\n * Byte storage that breaks byte sources into smaller byte sources. This storage uses another\n * storage as a delegate and, when a source is requested, it will allocate one or more sources from\n * the delegate to build the requested source.\n */\npublic class ChunkBasedByteStorage implements ByteStorage {\n\n  /** Size of the default chunk size. */\n  private static final long DEFAULT_CHUNK_SIZE_BYTES = 10 * 1024 * 1024;\n\n  /** Maximum size of each chunk. */\n  private final long maxChunkSize;\n\n  /** Byte storage where the data is actually stored. */\n  private final ByteStorage delegate;\n\n  /**\n   * Creates a new storage breaking sources in chunks with the default maximum size and allocating\n   * each chunk from {@code delegate}.\n   */\n  ChunkBasedByteStorage(ByteStorage delegate) {\n    this(DEFAULT_CHUNK_SIZE_BYTES, delegate);\n  }\n\n  /**\n   * Creates a new storage breaking sources in chunks with the maximum of {@code maxChunkSize} and\n   * allocating each chunk from {@code delegate}.\n   */\n  ChunkBasedByteStorage(long maxChunkSize, ByteStorage delegate) {\n    this.maxChunkSize = maxChunkSize;\n    this.delegate = delegate;\n  }\n\n  /** Obtains the byte storage chunks are allocated from. */\n  @VisibleForTesting // private otherwise.\n  public ByteStorage getDelegate() {\n    return delegate;\n  }\n\n  @Override\n  public CloseableByteSource fromStream(InputStream stream) throws IOException {\n    List<CloseableByteSource> sources = new ArrayList<>();\n    while (true) {\n      LimitedInputStream limitedInput = new LimitedInputStream(stream, maxChunkSize);\n      sources.add(delegate.fromStream(limitedInput));\n      if (limitedInput.isInputFinished()) {\n        break;\n      }\n    }\n\n    return new ChunkBasedCloseableByteSource(sources);\n  }\n\n  @Override\n  public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {\n    return new AbstractCloseableByteSourceFromOutputStreamBuilder() {\n      private final List<CloseableByteSource> sources = new ArrayList<>();\n      @Nullable private CloseableByteSourceFromOutputStreamBuilder currentBuilder = null;\n      private long written = 0;\n\n      @Override\n      protected void doWrite(byte[] b, int off, int len) throws IOException {\n        int actualOffset = off;\n        int remaining = len;\n\n        while (remaining > 0) {\n          // Since we're writing data, make sure we have a builder to create the new source.\n          if (currentBuilder == null) {\n            currentBuilder = delegate.makeBuilder();\n            written = 0;\n          }\n\n          // See how much we can write without exceeding maxChunkSize in the current builder.\n          int maxWrite = (int) Math.min(maxChunkSize - written, remaining);\n          currentBuilder.write(b, actualOffset, maxWrite);\n          written += maxWrite;\n\n          remaining -= maxWrite;\n          actualOffset += maxWrite;\n\n          // If we've reached the end of the chunk, create the source for the part we have and reset\n          // to builder so we start a new one if there is more data.\n          if (written == maxChunkSize) {\n            sources.add(currentBuilder.build());\n            currentBuilder = null;\n          }\n        }\n      }\n\n      @Override\n      protected CloseableByteSource doBuild() throws IOException {\n        // If we were writing a chunk, close it.\n        if (currentBuilder != null) {\n          sources.add(currentBuilder.build());\n          currentBuilder = null;\n        }\n\n        return new ChunkBasedCloseableByteSource(sources);\n      }\n    };\n  }\n\n  @Override\n  public CloseableByteSource fromSource(ByteSource source) throws IOException {\n    List<CloseableByteSource> sources = new ArrayList<>();\n\n    long end = source.size();\n    long start = 0;\n    while (start < end) {\n      long chunkSize = Math.min(end - start, maxChunkSize);\n      sources.add(delegate.fromSource(source.slice(start, chunkSize)));\n      start += chunkSize;\n    }\n\n    return new ChunkBasedCloseableByteSource(sources);\n  }\n\n  @Override\n  public long getBytesUsed() {\n    return delegate.getBytesUsed();\n  }\n\n  @Override\n  public long getMaxBytesUsed() {\n    return delegate.getMaxBytesUsed();\n  }\n\n  @Override\n  public void close() throws IOException {\n    delegate.close();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ChunkBasedByteStorageFactory.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/**\n * {@link ByteStorageFactory} that creates {@link ByteStorage} instances that keep all data in\n * memory.\n */\npublic class ChunkBasedByteStorageFactory implements ByteStorageFactory {\n\n  /** Factory to create the delegate storages. */\n  private final ByteStorageFactory delegate;\n\n  /** Maximum size for chunks, if any. */\n  @Nullable private final Long maxChunkSize;\n\n  /** Creates a new factory whose storages are created using delegates from the given factory. */\n  public ChunkBasedByteStorageFactory(ByteStorageFactory delegate) {\n    this(delegate, /*maxChunkSize=*/ null);\n  }\n\n  /**\n   * Creates a new factory whose storages use the given maximum chunk size and are created using\n   * delegates from the given factory.\n   */\n  public ChunkBasedByteStorageFactory(ByteStorageFactory delegate, @Nullable Long maxChunkSize) {\n    this.delegate = delegate;\n    this.maxChunkSize = maxChunkSize;\n  }\n\n  @Override\n  public ByteStorage create() throws IOException {\n    if (maxChunkSize == null) {\n      return new ChunkBasedByteStorage(delegate.create());\n    } else {\n      return new ChunkBasedByteStorage(maxChunkSize, delegate.create());\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/ChunkBasedCloseableByteSource.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.io.ByteSource;\nimport com.google.common.io.Closer;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * Byte source that has its data spread over several chunks, each with its own {@link\n * CloseableByteSource}.\n */\nclass ChunkBasedCloseableByteSource extends CloseableDelegateByteSource {\n\n  /** The sources for data of all the chunks, in order. */\n  private final ImmutableList<CloseableByteSource> sources;\n\n  /** Creates a new source from the given sources. */\n  ChunkBasedCloseableByteSource(List<CloseableByteSource> sources) throws IOException {\n    super(ByteSource.concat(sources), sumSizes(sources));\n    this.sources = ImmutableList.copyOf(sources);\n  }\n\n  /** Computes the size of this source by summing the sizes of all sources. */\n  private static long sumSizes(List<CloseableByteSource> sources) throws IOException {\n    long sum = 0;\n    for (CloseableByteSource source : sources) {\n      sum += source.size();\n    }\n\n    return sum;\n  }\n\n  @Override\n  protected synchronized void innerClose() throws IOException {\n    try (Closer closer = Closer.create()) {\n      for (CloseableByteSource source : sources) {\n        closer.register(source);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/CloseableByteSourceFromOutputStreamBuilder.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * Output stream that creates a {@link CloseableByteSource} from the data that was written to it.\n * Calling {@link #close} is optional as {@link #build()} will also close the output stream.\n */\npublic abstract class CloseableByteSourceFromOutputStreamBuilder extends OutputStream {\n\n  /**\n   * Creates the source from the data that has been written to the stream. No more data can be\n   * written to the output stream after this method has been called.\n   *\n   * @return a source that will provide the data that was written to the stream before this method\n   *     is invoked; where this data is stored is not specified by this interface\n   * @throws IOException failed to build the byte source\n   */\n  public abstract CloseableByteSource build() throws IOException;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/InMemoryByteStorage.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;\nimport com.google.common.io.ByteSource;\nimport com.google.common.io.ByteStreams;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/** Keeps track of used bytes allowing gauging memory usage. */\npublic class InMemoryByteStorage implements ByteStorage {\n\n  /** Number of bytes currently in use. */\n  private long bytesUsed;\n\n  /** Maximum number of bytes used. */\n  private long maxBytesUsed;\n\n  @Override\n  public CloseableByteSource fromStream(InputStream stream) throws IOException {\n    byte[] data = ByteStreams.toByteArray(stream);\n    updateUsage(data.length);\n    return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {\n      @Override\n      public synchronized void innerClose() throws IOException {\n        super.innerClose();\n        updateUsage(-sizeNoException());\n      }\n    };\n  }\n\n  @Override\n  public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {\n    ByteArrayOutputStream output = new ByteArrayOutputStream();\n    return new AbstractCloseableByteSourceFromOutputStreamBuilder() {\n      @Override\n      protected void doWrite(byte[] b, int off, int len) throws IOException {\n        output.write(b, off, len);\n        updateUsage(len);\n      }\n\n      @Override\n      protected CloseableByteSource doBuild() throws IOException {\n        byte[] data = output.toByteArray();\n        return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {\n          @Override\n          protected synchronized void innerClose() throws IOException {\n            super.innerClose();\n            updateUsage(-data.length);\n          }\n        };\n      }\n    };\n  }\n\n  @Override\n  public CloseableByteSource fromSource(ByteSource source) throws IOException {\n    return fromStream(source.openStream());\n  }\n\n  /**\n   * Updates the memory used by this tracker.\n   *\n   * @param delta the number of bytes to add or remove, if negative\n   */\n  private synchronized void updateUsage(long delta) {\n    bytesUsed += delta;\n    if (maxBytesUsed < bytesUsed) {\n      maxBytesUsed = bytesUsed;\n    }\n  }\n\n  @Override\n  public synchronized long getBytesUsed() {\n    return bytesUsed;\n  }\n\n  @Override\n  public synchronized long getMaxBytesUsed() {\n    return maxBytesUsed;\n  }\n\n  @Override\n  public void close() throws IOException {\n    // Nothing to do on close.\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/InMemoryByteStorageFactory.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport java.io.IOException;\n\n/**\n * {@link ByteStorageFactory} that creates {@link ByteStorage} instances that keep all data in\n * memory.\n */\npublic class InMemoryByteStorageFactory implements ByteStorageFactory {\n\n  @Override\n  public ByteStorage create() throws IOException {\n    return new InMemoryByteStorage();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/LimitedInputStream.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.bytestorage;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Input stream that reads only a limited number of bytes from another input stream before reporting\n * EOF. When closed, this stream will not close the underlying stream.\n *\n * <p>If the underlying stream does not have enough data, this stream will read all available data\n * from the underlying stream.\n */\nclass LimitedInputStream extends InputStream {\n  /** Where the data comes from. */\n  private final InputStream input;\n\n  /** How many bytes remain in this stream. */\n  private long remaining;\n\n  /** Has EOF been detected in {@link #input}? */\n  private boolean eofDetected;\n\n  /**\n   * Creates a new input stream.\n   *\n   * @param input where to read data from\n   * @param maximum the maximum number of bytes to read from {@code input}\n   */\n  LimitedInputStream(InputStream input, long maximum) {\n    this.input = input;\n    this.remaining = maximum;\n    this.eofDetected = false;\n  }\n\n  @Override\n  public int read() throws IOException {\n    if (remaining == 0) {\n      return -1;\n    }\n\n    int r = input.read();\n    if (r >= 0) {\n      remaining--;\n    } else {\n      eofDetected = true;\n    }\n\n    return r;\n  }\n\n  @Override\n  public int read(byte[] whereTo, int offset, int length) throws IOException {\n    if (remaining == 0) {\n      return -1;\n    }\n\n    int toRead = (int) Math.min(remaining, length);\n    int r = input.read(whereTo, offset, toRead);\n    if (r >= 0) {\n      remaining -= r;\n    } else {\n      eofDetected = true;\n    }\n\n    return r;\n  }\n\n  /** Returns {@code true} if EOF has been detected in the {@code input} stream. */\n  boolean isInputFinished() {\n    return eofDetected;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/LruTrackedCloseableByteSource.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.base.Preconditions;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Byte source that, until switched, will keep itself in the LRU queue. The byte source will\n * automatically remove itself from the queue once closed or moved to disk (see {@link\n * #moveToDisk(ByteStorage)}. This source should not be switched explicitly or tracking will not\n * work.\n *\n * <p>The source will consider an access to be opening a stream. Every time a stream is open the\n * source will move itself to the top of the LRU list.\n */\nclass LruTrackedCloseableByteSource extends SwitchableDelegateCloseableByteSource {\n  /** The tracker being used. */\n  private final LruTracker<LruTrackedCloseableByteSource> tracker;\n\n  /** Are we still tracking usage? */\n  private boolean tracking;\n\n  /** Has the byte source been closed? */\n  private boolean closed;\n\n  /** Creates a new byte source based on the given source and using the provided tracker. */\n  LruTrackedCloseableByteSource(\n      CloseableByteSource delegate, LruTracker<LruTrackedCloseableByteSource> tracker)\n      throws IOException {\n    super(delegate);\n    this.tracker = tracker;\n    tracker.track(this);\n    tracking = true;\n    closed = false;\n  }\n\n  @Override\n  public synchronized InputStream openStream() throws IOException {\n    Preconditions.checkState(!closed);\n    if (tracking) {\n      tracker.access(this);\n    }\n\n    return super.openStream();\n  }\n\n  @Override\n  protected synchronized void innerClose() throws IOException {\n    closed = true;\n\n    untrack();\n    super.innerClose();\n  }\n\n  /**\n   * Marks this source as not being tracked any more. May be called multiple times (only the first\n   * one will do anything).\n   */\n  private synchronized void untrack() {\n    if (tracking) {\n      tracking = false;\n      tracker.untrack(this);\n    }\n  }\n\n  /**\n   * Moves the contents of this source to a storage. This will untrack the source and switch its\n   * contents to a new delegate provided by {@code diskStorage}.\n   */\n  synchronized void move(ByteStorage diskStorage) throws IOException {\n    if (closed) {\n      return;\n    }\n\n    CloseableByteSource diskSource = diskStorage.fromSource(this);\n    untrack();\n    switchSource(diskSource);\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/LruTracker.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.BiMap;\nimport com.google.common.collect.HashBiMap;\nimport java.util.TreeSet;\nimport javax.annotation.Nullable;\n\n/**\n * A tracker that keeps a list of the last-recently-used objects of type {@code T}. The tracker\n * doesn't define what LRU means, it has a method, {@link #access(Object)} that marks the object as\n * being accessed and moves it to the top of the queue.\n *\n * <p>This implementation is O(log(N)) on all operations.\n *\n * <p>Implementation note: we don't keep track of time. Instead we use a counter that is incremented\n * every time a new access is done or a new object is tracked. Because of this, each access time is\n * unique for each object (although it will change after each access).\n */\nclass LruTracker<T> {\n\n  /** Maps each object to its unique access time and vice-versa. */\n  private final BiMap<T, Integer> objectToAccessTime;\n\n  /**\n   * Ordered set of all object's access times. This set has the same contents as {@code\n   * objectToAccessTime.value()}. It is sorted from the highest access time (newest) to the lowest\n   * access time (oldest).\n   */\n  private final TreeSet<Integer> accessTimes;\n\n  /** Next access time to use for tracking or accessing. */\n  private int currentTime;\n\n  /** Creates a new tracker without any objects. */\n  LruTracker() {\n    currentTime = 1;\n    objectToAccessTime = HashBiMap.create();\n    accessTimes = new TreeSet<>((i0, i1) -> i1 - i0);\n  }\n\n  /** Starts tracking an object. This object's will be the most recently used. */\n  synchronized void track(T object) {\n    Preconditions.checkState(!objectToAccessTime.containsKey(object));\n    objectToAccessTime.put(object, currentTime);\n    accessTimes.add(currentTime);\n    currentTime++;\n  }\n\n  /** Stops tracking an object. */\n  synchronized void untrack(T object) {\n    Preconditions.checkState(objectToAccessTime.containsKey(object));\n    accessTimes.remove(objectToAccessTime.get(object));\n    objectToAccessTime.remove(object);\n  }\n\n  /** Marks the given object as having been accessed promoting it as the most recently used. */\n  synchronized void access(T object) {\n    untrack(object);\n    track(object);\n  }\n\n  /**\n   * Obtains the position of an object in the queue. It will be {@code 0} for the most recently used\n   * object.\n   */\n  synchronized int positionOf(T object) {\n    Preconditions.checkState(objectToAccessTime.containsKey(object));\n    int lastAccess = objectToAccessTime.get(object);\n    return accessTimes.headSet(lastAccess).size();\n  }\n\n  /**\n   * Obtains the last element, the one last accessed earliest. Will return empty if there are no\n   * objects being tracked.\n   */\n  @Nullable\n  synchronized T last() {\n    if (accessTimes.isEmpty()) {\n      return null;\n    }\n\n    return objectToAccessTime.inverse().get(accessTimes.last());\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/OverflowToDiskByteStorage.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.io.ByteSource;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Byte storage that keeps data in memory up to a certain size. After that, older sources are moved\n * to disk and the newer ones served from memory.\n *\n * <p>Once unloaded to disk, sources are not reloaded into memory as that would be in direct\n * conflict with the filesystem's caching and the costs would probably outweight the benefits.\n *\n * <p>The maximum memory used by storage is actually larger than the maximum provided. It may exceed\n * the limit by the size of one source. That is because sources are always loaded into memory before\n * the storage decides to flush them to disk.\n */\npublic class OverflowToDiskByteStorage implements ByteStorage {\n\n  /** Size of the default memory cache. */\n  private static final long DEFAULT_MEMORY_CACHE_BYTES = 50 * 1024 * 1024;\n\n  /** In-memory storage. */\n  private final InMemoryByteStorage memoryStorage;\n\n  /** Disk-based storage. */\n  @VisibleForTesting // private otherwise.\n  final TemporaryDirectoryStorage diskStorage;\n\n  /** Tracker that keeps all memory sources. */\n  private final LruTracker<LruTrackedCloseableByteSource> memorySourcesTracker;\n\n  /** Maximum amount of data to keep in memory. */\n  private final long memoryCacheSize;\n\n  /** Maximum amount of data used. */\n  private long maxBytesUsed;\n\n  /**\n   * Creates a new byte storage with the default memory cache using the provided temporary directory\n   * to write data that overflows the memory size.\n   *\n   * @param temporaryDirectoryFactory the factory used to create a temporary directory where to\n   *     overflow to; the created directory will be closed when the {@link\n   *     OverflowToDiskByteStorage} object is closed\n   * @throws IOException failed to create the temporary directory\n   */\n  public OverflowToDiskByteStorage(TemporaryDirectoryFactory temporaryDirectoryFactory)\n      throws IOException {\n    this(DEFAULT_MEMORY_CACHE_BYTES, temporaryDirectoryFactory);\n  }\n\n  /**\n   * Creates a new byte storage with the given memory cache size using the provided temporary\n   * directory to write data that overflows the memory size.\n   *\n   * @param memoryCacheSize the in-memory cache; a value of {@link 0} will effectively disable\n   *     in-memory caching\n   * @param temporaryDirectoryFactory the factory used to create a temporary directory where to\n   *     overflow to; the created directory will be closed when the {@link\n   *     OverflowToDiskByteStorage} object is closed\n   * @throws IOException failed to create the temporary directory\n   */\n  public OverflowToDiskByteStorage(\n      long memoryCacheSize, TemporaryDirectoryFactory temporaryDirectoryFactory)\n      throws IOException {\n    memoryStorage = new InMemoryByteStorage();\n    diskStorage = new TemporaryDirectoryStorage(temporaryDirectoryFactory);\n    this.memoryCacheSize = memoryCacheSize;\n    this.memorySourcesTracker = new LruTracker<>();\n  }\n\n  @Override\n  public CloseableByteSource fromStream(InputStream stream) throws IOException {\n    CloseableByteSource memSource =\n        new LruTrackedCloseableByteSource(memoryStorage.fromStream(stream), memorySourcesTracker);\n    checkMaxUsage();\n    reviewSources();\n    return memSource;\n  }\n\n  @Override\n  public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {\n    CloseableByteSourceFromOutputStreamBuilder memBuilder = memoryStorage.makeBuilder();\n    return new AbstractCloseableByteSourceFromOutputStreamBuilder() {\n      @Override\n      protected void doWrite(byte[] b, int off, int len) throws IOException {\n        memBuilder.write(b, off, len);\n      }\n\n      @Override\n      protected CloseableByteSource doBuild() throws IOException {\n        CloseableByteSource memSource =\n            new LruTrackedCloseableByteSource(memBuilder.build(), memorySourcesTracker);\n        checkMaxUsage();\n        reviewSources();\n        return memSource;\n      }\n    };\n  }\n\n  @Override\n  public CloseableByteSource fromSource(ByteSource source) throws IOException {\n    CloseableByteSource memSource =\n        new LruTrackedCloseableByteSource(memoryStorage.fromSource(source), memorySourcesTracker);\n    checkMaxUsage();\n    reviewSources();\n    return memSource;\n  }\n\n  @Override\n  public synchronized long getBytesUsed() {\n    return memoryStorage.getBytesUsed() + diskStorage.getBytesUsed();\n  }\n\n  @Override\n  public synchronized long getMaxBytesUsed() {\n    return maxBytesUsed;\n  }\n\n  /** Checks if we have reached a new high of data usage and set it. */\n  private synchronized void checkMaxUsage() {\n    if (getBytesUsed() > maxBytesUsed) {\n      maxBytesUsed = getBytesUsed();\n    }\n  }\n\n  /** Checks if any of the sources needs to be written to disk or loaded into memory. */\n  private synchronized void reviewSources() throws IOException {\n    // Move data from memory to disk until we have at most memoryCacheSize bytes in memory.\n    while (memoryStorage.getBytesUsed() > memoryCacheSize) {\n      LruTrackedCloseableByteSource last = memorySourcesTracker.last();\n      if (last != null) {\n        LruTrackedCloseableByteSource lastSource = last;\n        lastSource.move(diskStorage);\n      }\n    }\n  }\n\n  /** Obtains the number of bytes stored in memory. */\n  public long getMemoryBytesUsed() {\n    return memoryStorage.getBytesUsed();\n  }\n\n  /** Obtains the maximum number of bytes ever stored in memory. */\n  public long getMaxMemoryBytesUsed() {\n    return memoryStorage.getMaxBytesUsed();\n  }\n\n  /** Obtains the number of bytes stored in disk. */\n  public long getDiskBytesUsed() {\n    return diskStorage.getBytesUsed();\n  }\n\n  /** Obtains the maximum number of bytes ever stored in disk. */\n  public long getMaxDiskBytesUsed() {\n    return diskStorage.getMaxBytesUsed();\n  }\n\n  @Override\n  public void close() throws IOException {\n    memoryStorage.close();\n    diskStorage.close();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/OverflowToDiskByteStorageFactory.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/**\n * {@link ByteStorageFactory} that creates instances of {@link ByteStorage} that will keep some data\n * in memory and will overflow to disk when necessary.\n */\npublic class OverflowToDiskByteStorageFactory implements ByteStorageFactory {\n\n  /** How much data we want to keep in cache? If {@code null} then we want the default value. */\n  @Nullable private final Long memoryCacheSizeInBytes;\n\n  /** Factory that creates temporary directories. */\n  private final TemporaryDirectoryFactory temporaryDirectoryFactory;\n\n  /**\n   * Creates a new factory with an optional in-memory size and a temporary directory for overflow.\n   *\n   * @param temporaryDirectoryFactory a factory that creates temporary directories that will be used\n   *     for overflow of the {@link ByteStorage} instances created by this factory\n   */\n  public OverflowToDiskByteStorageFactory(TemporaryDirectoryFactory temporaryDirectoryFactory) {\n    this(null, temporaryDirectoryFactory);\n  }\n\n  /**\n   * Creates a new factory with an optional in-memory size and a temporary directory for overflow.\n   *\n   * @param memoryCacheSizeInBytes how many bytes to keep in memory? If {@code null} then a default\n   *     value will be used\n   * @param temporaryDirectoryFactory a factory that creates temporary directories that will be used\n   *     for overflow of the {@link ByteStorage} instances created by this factory\n   */\n  public OverflowToDiskByteStorageFactory(\n      Long memoryCacheSizeInBytes, TemporaryDirectoryFactory temporaryDirectoryFactory) {\n    this.memoryCacheSizeInBytes = memoryCacheSizeInBytes;\n    this.temporaryDirectoryFactory = temporaryDirectoryFactory;\n  }\n\n  @Override\n  public ByteStorage create() throws IOException {\n    if (memoryCacheSizeInBytes == null) {\n      return new OverflowToDiskByteStorage(temporaryDirectoryFactory);\n    } else {\n      return new OverflowToDiskByteStorage(memoryCacheSizeInBytes, temporaryDirectoryFactory);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/SwitchableDelegateCloseableByteSource.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.io.Closer;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Byte source that delegates to another byte source that can be switched dynamically.\n *\n * <p>This byte source encloses another byte source (the delegate) and allows switching the\n * delegate. Switching is done transparently for the user (as long as the new byte source represents\n * the same data) maintaining all open streams working, but now streaming from the new source.\n */\nclass SwitchableDelegateCloseableByteSource extends CloseableByteSource {\n\n  /** The current delegate. */\n  private CloseableByteSource delegate;\n\n  /** Has the byte source been closed? */\n  private boolean closed;\n\n  /**\n   * Streams that have been opened, but not yet closed. These are all the streams that have to be\n   * switched when we switch delegates.\n   */\n  private final List<SwitchableDelegateInputStream> nonClosedStreams;\n\n  /** Creates a new source using {@code source} as delegate. */\n  SwitchableDelegateCloseableByteSource(CloseableByteSource source) {\n    this.delegate = source;\n    nonClosedStreams = new ArrayList<>();\n  }\n\n  @Override\n  protected synchronized void innerClose() throws IOException {\n    closed = true;\n\n    try (Closer closer = Closer.create()) {\n      for (SwitchableDelegateInputStream stream : nonClosedStreams) {\n        closer.register(stream);\n      }\n\n      nonClosedStreams.clear();\n    }\n\n    delegate.close();\n  }\n\n  @Override\n  public synchronized InputStream openStream() throws IOException {\n    SwitchableDelegateInputStream stream =\n        new SwitchableDelegateInputStream(delegate.openStream()) {\n          // Can't have a lock on the stream while we synchronize the removal of nonClosedStreams\n          // because it can deadlock when called in parallel with switchSource as the lock order is\n          // reversed. The lack of synchronization is OK because we don't access any data on the\n          // stream anyway until super.close() is called.\n          @SuppressWarnings(\"UnsynchronizedOverridesSynchronized\")\n          @Override\n          public void close() throws IOException {\n            // Remove the stream on close.\n            synchronized (SwitchableDelegateCloseableByteSource.this) {\n              nonClosedStreams.remove(this);\n            }\n\n            super.close();\n          }\n        };\n\n    nonClosedStreams.add(stream);\n    return stream;\n  }\n\n  /**\n   * Switches the current source for {@code source}. All streams are kept valid. The current source\n   * is closed.\n   *\n   * <p>If the current source has already been closed, {@code source} will also be closed and\n   * nothing else is done.\n   *\n   * <p>Otherwise, as long as it is possible to open enough input streams from {@code source} to\n   * replace all current input streams, the source if changed. Any errors while closing input\n   * streams (which happens during switching -- see {@link\n   * SwitchableDelegateInputStream#switchStream(InputStream)}) or closing the old source are\n   * reported as thrown {@code IOException}\n   */\n  synchronized void switchSource(CloseableByteSource source) throws IOException {\n    if (source == delegate) {\n      return;\n    }\n\n    if (closed) {\n      source.close();\n      return;\n    }\n\n    List<InputStream> switchStreams = new ArrayList<>();\n    for (int i = 0; i < nonClosedStreams.size(); i++) {\n      switchStreams.add(source.openStream());\n    }\n\n    CloseableByteSource oldDelegate = delegate;\n    delegate = source;\n\n    // A bit of trickery. We want to call switchStream for all streams. switchStream will\n    // successfully switch the stream even if it throws an exception (if it does, it means it\n    // failed to close the old stream). So we want to continue switching and recording all\n    // exceptions. Closer() has that logic already so we register each stream switch as a close\n    // operation.\n    try (Closer closer = Closer.create()) {\n      for (int i = 0; i < nonClosedStreams.size(); i++) {\n        SwitchableDelegateInputStream nonClosedStream = nonClosedStreams.get(i);\n        InputStream switchStream = switchStreams.get(i);\n        closer.register(() -> nonClosedStream.switchStream(switchStream));\n      }\n\n      closer.register(oldDelegate);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/SwitchableDelegateInputStream.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Input stream that delegates to another input stream, but can switch transparently the source\n * input stream.\n *\n * <p>Given a set of input streams that return the same data, this input stream will read from one\n * and allow switching to read from other streams continuing from the offset that was initially\n * read. The result is only meaningful if all streams read the same data.\n *\n * <p>This class allows transparently to switch between different implementations of the underlying\n * streams (memory, disk, etc.) while transparently providing data to users. It does not support\n * marking and it is multi-thread safe.\n */\nclass SwitchableDelegateInputStream extends InputStream {\n\n  /** The input stream that is currently providing data. */\n  private InputStream delegate;\n\n  /**\n   * Current offset in the input stream. We keep track of this to allow skipping data when switching\n   * input streams.\n   */\n  private long currentOffset;\n\n  /** Have we reached the end of stream? */\n  @VisibleForTesting // private otherwise.\n  boolean endOfStreamReached;\n\n  /**\n   * If a switch has occurred, how many bytes still need to be skipped in the input stream to\n   * continue reading from the same position?\n   */\n  private long needsSkipping;\n\n  SwitchableDelegateInputStream(InputStream delegate) {\n    this.delegate = delegate;\n    currentOffset = 0;\n    endOfStreamReached = false;\n    needsSkipping = 0;\n  }\n\n  /**\n   * Skips data in the input stream if it has been switched and there is data to skip. Will fail if\n   * we can't skip all the data.\n   */\n  private void skipDataIfNeeded() throws IOException {\n    while (needsSkipping > 0) {\n      long skipped = delegate.skip(needsSkipping);\n      if (skipped == 0) {\n        throw new IOException(\"Skipping InputStream after switching failed\");\n      }\n\n      needsSkipping -= skipped;\n    }\n  }\n\n  /** Same as {@link #increaseOffset(long)}. */\n  private int increaseOffset(int amount) {\n    return (int) increaseOffset((long) amount);\n  }\n\n  /**\n   * Increases the current offset after reading. {@code amount} will indicate how many bytes we have\n   * read. It {@code -1} then we know we've reached the end of the stream and {@link\n   * #endOfStreamReached} is set to {@code true}.\n   */\n  private long increaseOffset(long amount) {\n    if (amount > 0) {\n      currentOffset += amount;\n    }\n\n    if (amount == -1) {\n      endOfStreamReached = true;\n    }\n\n    return amount;\n  }\n\n  @Override\n  public synchronized int read(byte[] b) throws IOException {\n    if (endOfStreamReached) {\n      return -1;\n    }\n\n    skipDataIfNeeded();\n    return increaseOffset(delegate.read(b));\n  }\n\n  @Override\n  public synchronized int read(byte[] b, int off, int len) throws IOException {\n    if (endOfStreamReached) {\n      return -1;\n    }\n\n    skipDataIfNeeded();\n    return increaseOffset(delegate.read(b, off, len));\n  }\n\n  @Override\n  public synchronized int read() throws IOException {\n    if (endOfStreamReached) {\n      return -1;\n    }\n\n    skipDataIfNeeded();\n    int r = delegate.read();\n    if (r == -1) {\n      endOfStreamReached = true;\n    } else {\n      increaseOffset(1);\n    }\n\n    return r;\n  }\n\n  @Override\n  public synchronized long skip(long n) throws IOException {\n    if (endOfStreamReached) {\n      return 0;\n    }\n\n    skipDataIfNeeded();\n    return increaseOffset(delegate.skip(n));\n  }\n\n  @Override\n  public synchronized int available() throws IOException {\n    if (endOfStreamReached) {\n      return 0;\n    }\n\n    skipDataIfNeeded();\n    return delegate.available();\n  }\n\n  @Override\n  public synchronized void close() throws IOException {\n    endOfStreamReached = true;\n    delegate.close();\n  }\n\n  @Override\n  public void mark(int readlimit) {\n    // We don't support marking.\n  }\n\n  @Override\n  public void reset() throws IOException {\n    throw new IOException(\"Mark not supported\");\n  }\n\n  @Override\n  public boolean markSupported() {\n    return false;\n  }\n\n  /**\n   * Switches the stream used.\n   *\n   * <p>The stream that is currently in use and the new stream will be used in further operations.\n   * If this stream has already reached the end, {@code newStream} will be closed immediately and no\n   * other action is taken. If the stream has not reached the end, any exception reported is due to\n   * closing the stream currently in use, the new stream is not affected and this stream can still\n   * be used to read from {@code newStream}.\n   */\n  synchronized void switchStream(InputStream newStream) throws IOException {\n    if (newStream == delegate) {\n      return;\n    }\n\n    try (InputStream oldDelegate = delegate) {\n      delegate = newStream;\n      needsSkipping = currentOffset;\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryDirectory.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.google.common.annotations.VisibleForTesting;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n/**\n * A temporary directory is a directory that creates temporary files. Upon close, all temporary\n * files are removed. Whether the directory itself is removed is dependent on the actual\n * implementation.\n */\npublic interface TemporaryDirectory extends Closeable {\n\n  /**\n   * Creates a new file in the directory. This method returns a new file that deleted, recreated,\n   * read and written freely by the caller. No assumptions are made on the contents of this file\n   * except that it will be deleted it if it still exists when the temporary directory is closed.\n   */\n  File newFile() throws IOException;\n\n  /** Obtains the directory, only useful for tests. */\n  @VisibleForTesting // private otherwise.\n  File getDirectory();\n\n  /**\n   * Creates a new temporary directory in the system's temporary directory. All files created will\n   * be created in this directory. The directory will be deleted (as long as all the files in it)\n   * when closed.\n   */\n  static TemporaryDirectory newSystemTemporaryDirectory() throws IOException {\n    Path tempDir = Files.createTempDirectory(\"tempdir_\");\n    TemporaryFile tempDirFile = new TemporaryFile(tempDir.toFile());\n    return new TemporaryDirectory() {\n      @Override\n      public File newFile() throws IOException {\n        return Files.createTempFile(tempDir, \"temp_\", \".data\").toFile();\n      }\n\n      @Override\n      public File getDirectory() {\n        return tempDir.toFile();\n      }\n\n      @Override\n      public void close() throws IOException {\n        tempDirFile.close();\n      }\n    };\n  }\n\n  /**\n   * Creates a new temporary directory that uses a fixed directory.\n   *\n   * @param directory the directory that will be returned; this directory won't be deleted when the\n   *     {@link TemporaryDirectory} objects are closed\n   * @return a {@link TemporaryDirectory} that will create files in {@code directory}\n   */\n  static TemporaryDirectory fixed(File directory) {\n    return new TemporaryDirectory() {\n      @Override\n      public File newFile() throws IOException {\n        return Files.createTempFile(directory.toPath(), \"temp_\", \".data\").toFile();\n      }\n\n      @Override\n      public File getDirectory() {\n        return directory;\n      }\n\n      @Override\n      public void close() throws IOException {}\n    };\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryDirectoryFactory.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * Factory that creates temporary directories. {@link\n * TemporaryDirectory#newSystemTemporaryDirectory()} conforms to this interface.\n */\npublic interface TemporaryDirectoryFactory {\n\n  /**\n   * Creates a new temporary directory.\n   *\n   * @return the new temporary directory that should be closed when finished\n   * @throws IOException failed to create the temporary directory\n   */\n  TemporaryDirectory make() throws IOException;\n\n  /**\n   * Obtains a factory that creates temporary directories using {@link\n   * TemporaryDirectory#fixed(File)}.\n   *\n   * @param directory the directory where all temporary files will be created\n   * @return a factory that creates instances of {@link TemporaryDirectory} that creates all files\n   *     inside {@code directory}\n   */\n  static TemporaryDirectoryFactory fixed(File directory) {\n    return () -> TemporaryDirectory.fixed(directory);\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryDirectoryStorage.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.io.ByteSource;\nimport com.google.common.io.ByteStreams;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Byte storage that keeps all byte sources as files in a temporary directory. Each data stored is\n * stored as a new file. The file is deleted as soon as the byte source is closed.\n */\npublic class TemporaryDirectoryStorage implements ByteStorage {\n\n  /** Temporary directory to use. */\n  @VisibleForTesting // private otherwise.\n  final TemporaryDirectory temporaryDirectory;\n\n  /** Number of bytes currently used. */\n  private long bytesUsed;\n\n  /** Maximum number of bytes used. */\n  private long maxBytesUsed;\n\n  /**\n   * Creates a new storage using the provided temporary directory.\n   *\n   * @param temporaryDirectoryFactory a factory used to create the directory to use for temporary\n   *     files; this directory will be closed when the {@link TemporaryDirectoryStorage} is closed.\n   * @throws IOException failed to create the temporary directory\n   */\n  public TemporaryDirectoryStorage(TemporaryDirectoryFactory temporaryDirectoryFactory)\n      throws IOException {\n    this.temporaryDirectory = temporaryDirectoryFactory.make();\n  }\n\n  @Override\n  public CloseableByteSource fromStream(InputStream stream) throws IOException {\n    File temporaryFile = temporaryDirectory.newFile();\n    try (FileOutputStream output = new FileOutputStream(temporaryFile)) {\n      ByteStreams.copy(stream, output);\n    }\n\n    long size = temporaryFile.length();\n    incrementBytesUsed(size);\n    return new TemporaryFileCloseableByteSource(temporaryFile, () -> incrementBytesUsed(-size));\n  }\n\n  @Override\n  public CloseableByteSourceFromOutputStreamBuilder makeBuilder() throws IOException {\n    File temporaryFile = temporaryDirectory.newFile();\n    return new AbstractCloseableByteSourceFromOutputStreamBuilder() {\n      private final FileOutputStream output = new FileOutputStream(temporaryFile);\n\n      @Override\n      protected void doWrite(byte[] b, int off, int len) throws IOException {\n        output.write(b, off, len);\n        incrementBytesUsed(len);\n      }\n\n      @Override\n      protected CloseableByteSource doBuild() throws IOException {\n        output.close();\n        long size = temporaryFile.length();\n        return new TemporaryFileCloseableByteSource(temporaryFile, () -> incrementBytesUsed(-size));\n      }\n    };\n  }\n\n  @Override\n  public CloseableByteSource fromSource(ByteSource source) throws IOException {\n    try (InputStream stream = source.openStream()) {\n      return fromStream(stream);\n    }\n  }\n\n  @Override\n  public synchronized long getBytesUsed() {\n    return bytesUsed;\n  }\n\n  @Override\n  public synchronized long getMaxBytesUsed() {\n    return maxBytesUsed;\n  }\n\n  /** Increments the byte counter by the given amount (decrements if {@code amount} is negative). */\n  private synchronized void incrementBytesUsed(long amount) {\n    bytesUsed += amount;\n    if (bytesUsed > maxBytesUsed) {\n      maxBytesUsed = bytesUsed;\n    }\n  }\n\n  @Override\n  public void close() throws IOException {\n    temporaryDirectory.close();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryFile.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.google.common.base.Preconditions;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * A temporary file or directory. Wraps a file or directory and deletes it (recursively, if it is a\n * directory) when closed.\n */\npublic class TemporaryFile implements Closeable {\n\n  /** Has the file or directory represented by {@link #file} been deleted? */\n  private boolean deleted;\n\n  /**\n   * The file or directory that will be deleted on close. May no longer exist if {@link #deleted} is\n   * {@code true}.\n   */\n  private final File file;\n\n  /**\n   * Creates a new wrapper around the given file. The file or directory {@code file} will be deleted\n   * (recursively, if it is a directory) on close.\n   */\n  public TemporaryFile(File file) {\n    deleted = false;\n    this.file = file;\n  }\n\n  /** Obtains the file or directory this temporary file refers to. */\n  public File getFile() {\n    Preconditions.checkState(!deleted, \"File already deleted\");\n    return file;\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (deleted) {\n      return;\n    }\n\n    deleted = true;\n\n    deleteFile(file);\n  }\n\n  /** Deletes a file or directory if it exists. */\n  private void deleteFile(File file) throws IOException {\n    if (file.isDirectory()) {\n      File[] contents = file.listFiles();\n      if (contents != null) {\n        for (File subFile : contents) {\n          deleteFile(subFile);\n        }\n      }\n    }\n\n    if (file.exists() && !file.delete()) {\n      throw new IOException(\"Failed to delete '\" + file.getAbsolutePath() + \"'\");\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/bytestorage/TemporaryFileCloseableByteSource.java",
    "content": "package com.android.tools.build.apkzlib.bytestorage;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;\nimport com.google.common.io.Files;\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * Closeable byte source that uses a temporary file to store its contents. The file is deleted when\n * the byte source is closed.\n */\nclass TemporaryFileCloseableByteSource extends CloseableDelegateByteSource {\n\n  /** Temporary file backing the byte source. */\n  private final TemporaryFile temporaryFile;\n\n  /** Callback to notify when the byte source is closed. */\n  private final Runnable closeCallback;\n\n  /**\n   * Creates a new byte source based on the given file. The provided callback is executed when the\n   * source is deleted. There is no guarantee about which thread invokes the callback (it is the\n   * thread that closes the source).\n   */\n  TemporaryFileCloseableByteSource(File file, Runnable closeCallback) {\n    super(Files.asByteSource(file), file.length());\n    temporaryFile = new TemporaryFile(file);\n    this.closeCallback = closeCallback;\n  }\n\n  @Override\n  protected synchronized void innerClose() throws IOException {\n    super.innerClose();\n    temporaryFile.close();\n    closeCallback.run();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/DigestAlgorithm.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.sign;\n\n\n/** Message digest algorithms. */\npublic enum DigestAlgorithm {\n  /**\n   * SHA-1 digest.\n   *\n   * <p>Android 2.3 (API Level 9) to 4.2 (API Level 17) (inclusive) do not support SHA-2 JAR\n   * signatures.\n   *\n   * <p>Moreover, platforms prior to API Level 18, without the additional Digest-Algorithms\n   * attribute, only support SHA or SHA1 algorithm names in .SF and MANIFEST.MF attributes.\n   */\n  SHA1(\"SHA1\", \"SHA-1\"),\n\n  /** SHA-256 digest. */\n  SHA256(\"SHA-256\", \"SHA-256\");\n\n  /**\n   * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and {@link\n   * SignatureAlgorithm#ECDSA}.\n   */\n  public static final int API_SHA_256_RSA_AND_ECDSA = 18;\n\n  /**\n   * API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s.\n   *\n   * <p>Before that, SHA256 can only be used with RSA and ECDSA.\n   */\n  public static final int API_SHA_256_ALL_ALGORITHMS = 21;\n\n  /** Name of algorithm for message digest. */\n  public final String messageDigestName;\n\n  /** Name of attribute in signature file with the manifest digest. */\n  public final String manifestAttributeName;\n\n  /** Name of attribute in entry (both manifest and signature file) with the entry's digest. */\n  public final String entryAttributeName;\n\n  /**\n   * Creates a digest algorithm.\n   *\n   * @param attributeName attribute name in the signature file\n   * @param messageDigestName name of algorithm for message digest\n   */\n  DigestAlgorithm(String attributeName, String messageDigestName) {\n    this.messageDigestName = messageDigestName;\n    this.entryAttributeName = attributeName + \"-Digest\";\n    this.manifestAttributeName = attributeName + \"-Digest-Manifest\";\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/ManifestGenerationExtension.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.sign;\n\nimport com.android.tools.build.apkzlib.utils.CachedSupplier;\nimport com.android.tools.build.apkzlib.utils.IOExceptionRunnable;\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.android.tools.build.apkzlib.zfile.ManifestAttributes;\nimport com.android.tools.build.apkzlib.zip.StoredEntry;\nimport com.android.tools.build.apkzlib.zip.ZFile;\nimport com.android.tools.build.apkzlib.zip.ZFileExtension;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.util.jar.Attributes;\nimport java.util.jar.Manifest;\nimport javax.annotation.Nullable;\n\n/**\n * Extension to {@link ZFile} that will generate a manifest. The extension will register\n * automatically with the {@link ZFile}.\n *\n * <p>Creating this extension will ensure a manifest for the zip exists. This extension will\n * generate a manifest if one does not exist and will update an existing manifest, if one does\n * exist. The extension will also provide access to the manifest so that others may update the\n * manifest.\n *\n * <p>Apart from standard manifest elements, this extension does not handle any particular manifest\n * features such as signing or adding custom attributes. It simply generates a plain manifest and\n * provides infrastructure so that other extensions can add data in the manifest.\n *\n * <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}\n * notification is received, meaning all manifest manipulation is done in-memory.\n */\npublic class ManifestGenerationExtension {\n\n  /** Name of META-INF directory. */\n  private static final String META_INF_DIR = \"META-INF\";\n\n  /** Name of the manifest file. */\n  static final String MANIFEST_NAME = META_INF_DIR + \"/MANIFEST.MF\";\n\n  /** Who should be reported as the manifest builder. */\n  private final String builtBy;\n\n  /** Who should be reported as the manifest creator. */\n  private final String createdBy;\n\n  /** The file this extension is attached to. {@code null} if not yet registered. */\n  @Nullable private ZFile zFile;\n\n  /** The zip file's manifest. */\n  private final Manifest manifest;\n\n  /**\n   * Byte representation of the manifest. There is no guarantee that two writes of the java's {@code\n   * Manifest} object will yield the same byte array (there is no guaranteed order of entries in the\n   * manifest).\n   *\n   * <p>Because we need the byte representation of the manifest to be stable if there are no changes\n   * to the manifest, we cannot rely on {@code Manifest} to generate the byte representation every\n   * time we need the byte representation.\n   *\n   * <p>This cache will ensure that we will request one byte generation from the {@code Manifest}\n   * and will cache it. All further requests of the manifest's byte representation will receive the\n   * same byte array.\n   */\n  private final CachedSupplier<byte[]> manifestBytes;\n\n  /**\n   * Has the current manifest been changed and not yet flushed? If {@link #dirty} is {@code true},\n   * then {@link #manifestBytes} should not be valid. This means that marking the manifest as dirty\n   * should also invalidate {@link #manifestBytes}. To avoid breaking the invariant, instead of\n   * setting {@link #dirty}, {@link #markDirty()} should be called.\n   */\n  private boolean dirty;\n\n  /** The extension to register with the {@link ZFile}. {@code null} if not registered. */\n  @Nullable private ZFileExtension extension;\n\n  /**\n   * Creates a new extension. This will not register the extension with the provided {@link ZFile}.\n   * Until {@link #register(ZFile)} is invoked, this extension is not used.\n   *\n   * @param builtBy who built the manifest?\n   * @param createdBy who created the manifest?\n   */\n  public ManifestGenerationExtension(String builtBy, String createdBy) {\n    this.builtBy = builtBy;\n    this.createdBy = createdBy;\n    manifest = new Manifest();\n    dirty = false;\n    manifestBytes =\n        new CachedSupplier<>(\n            () -> {\n              ByteArrayOutputStream outBytes = new ByteArrayOutputStream();\n              try {\n                manifest.write(outBytes);\n              } catch (IOException e) {\n                throw new IOExceptionWrapper(e);\n              }\n\n              return outBytes.toByteArray();\n            });\n  }\n\n  /**\n   * Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last read\n   * and/or written.\n   */\n  private void markDirty() {\n    dirty = true;\n    manifestBytes.reset();\n  }\n\n  /**\n   * Registers the extension with the {@link ZFile} provided in the constructor.\n   *\n   * @param zFile the zip file to add the extension to\n   * @throws IOException failed to analyze the zip\n   */\n  public void register(ZFile zFile) throws IOException {\n    Preconditions.checkState(extension == null, \"register() has already been invoked.\");\n    this.zFile = zFile;\n\n    rebuildManifest();\n\n    extension =\n        new ZFileExtension() {\n          @Nullable\n          @Override\n          public IOExceptionRunnable beforeUpdate() {\n            return ManifestGenerationExtension.this::updateManifest;\n          }\n        };\n\n    this.zFile.addZFileExtension(extension);\n  }\n\n  /** Rebuilds the zip file's manifest, if it needs changes. */\n  private void rebuildManifest() throws IOException {\n    Verify.verifyNotNull(zFile, \"zFile == null\");\n\n    StoredEntry manifestEntry = zFile.get(MANIFEST_NAME);\n\n    if (manifestEntry != null) {\n      /*\n       * Read the manifest entry in the zip file. Make sure we store these byte sequence\n       * because writing the manifest may not generate the same byte sequence, which may\n       * trigger an unnecessary re-sign of the jar.\n       */\n      manifest.clear();\n      byte[] manifestBytes = manifestEntry.read();\n      manifest.read(new ByteArrayInputStream(manifestBytes));\n      this.manifestBytes.precomputed(manifestBytes);\n    }\n\n    Attributes mainAttributes = manifest.getMainAttributes();\n    String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);\n    if (currentVersion == null) {\n      setMainAttribute(\n          ManifestAttributes.MANIFEST_VERSION, ManifestAttributes.CURRENT_MANIFEST_VERSION);\n    } else {\n      if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {\n        throw new IOException(\"Unsupported manifest version: \" + currentVersion + \".\");\n      }\n    }\n\n    /*\n     * We \"blindly\" override all other main attributes.\n     */\n    setMainAttribute(ManifestAttributes.BUILT_BY, builtBy);\n    setMainAttribute(ManifestAttributes.CREATED_BY, createdBy);\n  }\n\n  /**\n   * Sets the value of a main attribute.\n   *\n   * @param attribute the attribute\n   * @param value the value\n   */\n  private void setMainAttribute(String attribute, String value) {\n    Attributes mainAttributes = manifest.getMainAttributes();\n    String current = mainAttributes.getValue(attribute);\n    if (!value.equals(current)) {\n      mainAttributes.putValue(attribute, value);\n      markDirty();\n    }\n  }\n\n  /**\n   * Updates the manifest in the zip file, if it has been changed.\n   *\n   * @throws IOException failed to update the manifest\n   */\n  private void updateManifest() throws IOException {\n    Verify.verifyNotNull(zFile, \"zFile == null\");\n\n    if (!dirty) {\n      return;\n    }\n\n    zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get()));\n    dirty = false;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/SignatureAlgorithm.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.sign;\n\nimport java.security.NoSuchAlgorithmException;\n\n/** Signature algorithm. */\npublic enum SignatureAlgorithm {\n  /** RSA algorithm. */\n  RSA(\"RSA\", 1, \"withRSA\"),\n\n  /** ECDSA algorithm. */\n  ECDSA(\"EC\", 18, \"withECDSA\"),\n\n  /** DSA algorithm. */\n  DSA(\"DSA\", 1, \"withDSA\");\n\n  /** Name of the private key as reported by {@code PrivateKey}. */\n  public final String keyAlgorithm;\n\n  /** Minimum SDK version that allows this signature. */\n  public final int minSdkVersion;\n\n  /** Suffix appended to digest algorithm to obtain signature algorithm. */\n  public final String signatureAlgorithmSuffix;\n\n  /**\n   * Creates a new signature algorithm.\n   *\n   * @param keyAlgorithm the name as reported by {@code PrivateKey}\n   * @param minSdkVersion minimum SDK version that allows this signature\n   * @param signatureAlgorithmSuffix suffix for signature name with used with a digest\n   */\n  SignatureAlgorithm(String keyAlgorithm, int minSdkVersion, String signatureAlgorithmSuffix) {\n    this.keyAlgorithm = keyAlgorithm;\n    this.minSdkVersion = minSdkVersion;\n    this.signatureAlgorithmSuffix = signatureAlgorithmSuffix;\n  }\n\n  /**\n   * Obtains the signature algorithm that corresponds to a private key name applicable to a SDK\n   * version.\n   *\n   * @param keyAlgorithm the named referred in the {@code PrivateKey}\n   * @param minSdkVersion minimum SDK version to run\n   * @return the algorithm that has {@link #keyAlgorithm} equal to {@code keyAlgorithm}\n   * @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an\n   *     algorithm was found but is not applicable to the given SDK version\n   */\n  public static SignatureAlgorithm fromKeyAlgorithm(String keyAlgorithm, int minSdkVersion)\n      throws NoSuchAlgorithmException {\n    for (SignatureAlgorithm alg : values()) {\n      if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) {\n        if (alg.minSdkVersion > minSdkVersion) {\n          throw new NoSuchAlgorithmException(\n              \"Signatures with \"\n                  + keyAlgorithm\n                  + \" keys are not supported on minSdkVersion \"\n                  + minSdkVersion\n                  + \". They are supported only for minSdkVersion >= \"\n                  + alg.minSdkVersion);\n        }\n\n        return alg;\n      }\n    }\n\n    throw new NoSuchAlgorithmException(\"Signing with \" + keyAlgorithm + \" keys is not supported\");\n  }\n\n  /**\n   * Obtains the name of the signature algorithm when used with a digest algorithm.\n   *\n   * @param digestAlgorithm the digest algorithm to use\n   * @return the name of the signature algorithm\n   */\n  public String signatureAlgorithmName(DigestAlgorithm digestAlgorithm) {\n    return digestAlgorithm.messageDigestName.replace(\"-\", \"\") + signatureAlgorithmSuffix;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/SigningExtension.java",
    "content": "/*\n * Copyright (C) 2016 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 */\npackage com.android.tools.build.apkzlib.sign;\n\nimport com.android.apksig.ApkSignerEngine;\nimport com.android.apksig.ApkVerifier;\nimport com.android.apksig.DefaultApkSignerEngine;\nimport com.android.apksig.apk.ApkFormatException;\nimport com.android.apksig.internal.apk.ApkSigningBlockUtils;\nimport com.android.apksig.util.DataSink;\nimport com.android.apksig.util.DataSource;\nimport com.android.apksig.util.DataSources;\nimport com.android.tools.build.apkzlib.utils.IOExceptionRunnable;\nimport com.android.tools.build.apkzlib.utils.SigningBlockUtils;\nimport com.android.tools.build.apkzlib.zip.StoredEntry;\nimport com.android.tools.build.apkzlib.zip.ZFile;\nimport com.android.tools.build.apkzlib.zip.ZFileExtension;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Supplier;\nimport com.google.common.base.Suppliers;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.collect.ImmutableSet;\nimport com.google.common.collect.Iterables;\nimport com.google.common.primitives.Bytes;\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SignatureException;\nimport java.security.cert.CertificateEncodingException;\nimport java.security.cert.X509Certificate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport javax.annotation.Nullable;\n\n/**\n * {@link ZFile} extension which signs the APK.\n *\n * <p>This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK\n * Signature Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters\n * to this extension's constructor.\n */\npublic class SigningExtension {\n  private static final int MAX_READ_CHUNK_SIZE = 65536;\n\n  // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive\n  // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.\n  // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile\n  // extension events/callbacks.\n  //\n  // The main issue leading to additional complexity in this class is that the current build\n  // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that\n  // matter) for incremental builds. Thus:\n  // * ZFile extension receives no events for JAR entries already in the APK whereas\n  //   ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this\n  //   class, during \"beforeUpdate\" ZFile event, notifies ApkSignerEngine about JAR entries\n  //   already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR\n  //   entries which the incremental build session did not touch.\n  // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed\n  //   from it whereas ApkSignerEngine produces no output only if it has already produced a signed\n  //   APK and no changes have since been made to it. This class addresses this issue by checking\n  //   in its \"register\" method whether the APK is correctly signed and, only if that's the case,\n  //   doesn't modify the APK unless a JAR entry is added to it or removed from it after\n  //   \"register\".\n\n  /** APK signer which performs most of the heavy lifting. */\n  private final ApkSignerEngine signer;\n\n  /** Names of APK entries which have been processed by {@link #signer}. */\n  private final Set<String> signerProcessedOutputEntryNames = new HashSet<>();\n\n  /** Signing block Id for SDK dependency block. */\n  static final int DEPENDENCY_INFO_BLOCK_ID = 0x504b4453;\n\n  /** SDK dependencies of the APK */\n  @Nullable private byte[] sdkDependencyData;\n\n  /**\n   * Cached contents of the most recently output APK Signing Block or {@code null} if the block\n   * hasn't yet been output.\n   */\n  @Nullable private byte[] cachedApkSigningBlock;\n\n  /**\n   * {@code true} if signatures may need to be output, {@code false} if there's no need to output\n   * signatures. This is used in an optimization where we don't modify the APK if it's already\n   * signed and if no JAR entries have been added to or removed from the file.\n   */\n  private boolean dirty;\n\n  /** The extension registered with the {@link ZFile}. {@code null} if not registered. */\n  @Nullable private ZFileExtension extension;\n\n  /** The file this extension is attached to. {@code null} if not yet registered. */\n  @Nullable private ZFile zFile;\n\n  /** A buffer used to read data from entries to feed to digests */\n  private final Supplier<byte[]> digestBuffer =\n      Suppliers.memoize(() -> new byte[MAX_READ_CHUNK_SIZE]);\n\n  /** An object that has all necessary information to sign the zip file and verify its signature */\n  private final SigningOptions options;\n\n  public SigningExtension(SigningOptions opts) throws InvalidKeyException {\n    DefaultApkSignerEngine.SignerConfig signerConfig =\n        new DefaultApkSignerEngine.SignerConfig.Builder(\n                \"CERT\", opts.getKey(), opts.getCertificates())\n            .build();\n    signer =\n        new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), opts.getMinSdkVersion())\n            .setOtherSignersSignaturesPreserved(false)\n            .setV1SigningEnabled(opts.isV1SigningEnabled())\n            .setV2SigningEnabled(opts.isV2SigningEnabled())\n            .setV3SigningEnabled(false)\n            .setCreatedBy(\"1.0 (Android)\")\n            .build();\n    if (opts.getSdkDependencyData() != null) {\n      sdkDependencyData = opts.getSdkDependencyData();\n    }\n    if (opts.getExecutor() != null) {\n      signer.setExecutor(opts.getExecutor());\n    }\n    this.options = opts;\n  }\n\n  public void register(ZFile zFile) throws NoSuchAlgorithmException, IOException {\n    Preconditions.checkState(extension == null, \"register() already invoked\");\n    this.zFile = zFile;\n    switch (options.getValidation()) {\n      case ALWAYS_VALIDATE:\n        dirty = !isCurrentSignatureAsRequested();\n        break;\n      case ASSUME_VALID:\n        if (options.isV1SigningEnabled()) {\n          Set<String> entryNames =\n              ImmutableSet.copyOf(\n                  Iterables.transform(\n                      zFile.entries(), e -> e.getCentralDirectoryHeader().getName()));\n          StoredEntry manifestEntry = zFile.get(ManifestGenerationExtension.MANIFEST_NAME);\n\n          Preconditions.checkNotNull(\n              manifestEntry,\n              \"No manifest found in apk for incremental build with enabled v1 signature\");\n          signerProcessedOutputEntryNames.addAll(\n              this.signer.initWith(manifestEntry.read(), entryNames));\n        }\n\n        dirty = false;\n        break;\n      case ASSUME_INVALID:\n        dirty = true;\n        break;\n    }\n    extension =\n        new ZFileExtension() {\n          @Override\n          public IOExceptionRunnable added(StoredEntry entry, @Nullable StoredEntry replaced) {\n            return () -> onZipEntryOutput(entry);\n          }\n\n          @Override\n          public IOExceptionRunnable removed(StoredEntry entry) {\n            String entryName = entry.getCentralDirectoryHeader().getName();\n            return () -> onZipEntryRemovedFromOutput(entryName);\n          }\n\n          @Override\n          public IOExceptionRunnable beforeUpdate() throws IOException {\n            return () -> onOutputZipReadyForUpdate();\n          }\n\n          @Override\n          public void entriesWritten() throws IOException {\n            onOutputZipEntriesWritten();\n          }\n\n          @Override\n          public void closed() {\n            onOutputClosed();\n          }\n        };\n    this.zFile.addZFileExtension(extension);\n  }\n\n  /**\n   * Returns {@code true} if the APK's signatures are as requested by parameters to this signing\n   * extension.\n   */\n  private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {\n    ApkVerifier.Result result;\n    try {\n      result =\n          new ApkVerifier.Builder(zFile.asDataSource())\n              .setMinCheckedPlatformVersion(options.getMinSdkVersion())\n              .build()\n              .verify();\n    } catch (ApkFormatException e) {\n      // Malformed APK\n      return false;\n    }\n\n    if (!result.isVerified()) {\n      // Signature(s) did not verify\n      return false;\n    }\n\n    if ((result.isVerifiedUsingV1Scheme() != options.isV1SigningEnabled())\n        || (result.isVerifiedUsingV2Scheme() != options.isV2SigningEnabled())) {\n      // APK isn't signed with exactly the schemes we want it to be signed\n      return false;\n    }\n\n    List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();\n    if (verifiedSignerCerts.size() != 1) {\n      // APK is not signed by exactly one signer\n      return false;\n    }\n\n    byte[] expectedEncodedCert;\n    byte[] actualEncodedCert;\n    try {\n      expectedEncodedCert = options.getCertificates().get(0).getEncoded();\n      actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();\n    } catch (CertificateEncodingException e) {\n      // Failed to encode signing certificates\n      return false;\n    }\n\n    if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {\n      // APK is signed by a wrong signer\n      return false;\n    }\n\n    // APK is signed the way we want it to be signed\n    return true;\n  }\n\n  private void onZipEntryOutput(StoredEntry entry) throws IOException {\n    setDirty();\n    String entryName = entry.getCentralDirectoryHeader().getName();\n    // This event may arrive after the entry has already been deleted. In that case, we don't\n    // report the addition of the entry to ApkSignerEngine.\n    if (entry.isDeleted()) {\n      return;\n    }\n    ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = signer.outputJarEntry(entryName);\n    signerProcessedOutputEntryNames.add(entryName);\n    if (inspectEntryRequest != null) {\n      try (InputStream inputStream = new BufferedInputStream(entry.open())) {\n        copyStreamToDataSink(inputStream, inspectEntryRequest.getDataSink());\n      }\n      inspectEntryRequest.done();\n    }\n  }\n\n  private void copyStreamToDataSink(InputStream inputStream, DataSink dataSink) throws IOException {\n    int bytesRead;\n    byte[] buffer = digestBuffer.get();\n    while ((bytesRead = inputStream.read(buffer)) > 0) {\n      dataSink.consume(buffer, 0, bytesRead);\n    }\n  }\n\n  private void onZipEntryRemovedFromOutput(String entryName) {\n    setDirty();\n    signer.outputJarEntryRemoved(entryName);\n    signerProcessedOutputEntryNames.remove(entryName);\n  }\n\n  private void onOutputZipReadyForUpdate() throws IOException {\n    if (!dirty) {\n      return;\n    }\n\n    // Notify signer engine about ZIP entries that have appeared in the output without the\n    // engine knowing. Also identify ZIP entries which disappeared from the output without the\n    // engine knowing.\n    Set<String> unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames);\n    for (StoredEntry entry : zFile.entries()) {\n      String entryName = entry.getCentralDirectoryHeader().getName();\n      unprocessedRemovedEntryNames.remove(entryName);\n      if (!signerProcessedOutputEntryNames.contains(entryName)) {\n        // Signer engine is not yet aware that this entry is in the output\n        onZipEntryOutput(entry);\n      }\n    }\n\n    // Notify signer engine about entries which disappeared from the output without the engine\n    // knowing\n    for (String entryName : unprocessedRemovedEntryNames) {\n      onZipEntryRemovedFromOutput(entryName);\n    }\n\n    // Check whether we need to output additional JAR entries which comprise the v1 signature\n    ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;\n    try {\n      addV1SignatureRequest = signer.outputJarEntries();\n    } catch (Exception e) {\n      throw new IOException(\"Failed to generate v1 signature\", e);\n    }\n    if (addV1SignatureRequest == null) {\n      return;\n    }\n\n    // We need to output additional JAR entries which comprise the v1 signature\n    List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =\n        new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());\n\n    // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first\n    // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by\n    // ManifestGenerationExtension.\n    for (int i = 0; i < v1SignatureEntries.size(); i++) {\n      ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);\n      String name = entry.getName();\n      if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {\n        continue;\n      }\n      if (i != 0) {\n        v1SignatureEntries.remove(i);\n        v1SignatureEntries.add(0, entry);\n      }\n      break;\n    }\n\n    // Output the JAR entries comprising the v1 signature\n    for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {\n      String name = entry.getName();\n      byte[] data = entry.getData();\n      zFile.add(name, new ByteArrayInputStream(data));\n    }\n\n    addV1SignatureRequest.done();\n  }\n\n  private void onOutputZipEntriesWritten() throws IOException {\n    if (!dirty) {\n      return;\n    }\n\n    // Check whether we should output an APK Signing Block which contains v2 signatures\n    byte[] apkSigningBlock;\n    byte[] centralDirBytes = zFile.getCentralDirectoryBytes();\n    byte[] eocdBytes = zFile.getEocdBytes();\n    ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest;\n    // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we\n    // cache the block to speed things up. The cached block is invalidated by any changes to the\n    // file (as reported to this extension).\n    if (cachedApkSigningBlock != null) {\n      apkSigningBlock = cachedApkSigningBlock;\n      addV2SignatureRequest = null;\n    } else {\n      DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));\n      DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));\n      long zipEntriesSizeBytes =\n          zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset();\n      DataSource zipEntries = zFile.asDataSource(0, zipEntriesSizeBytes);\n      try {\n        addV2SignatureRequest = signer.outputZipSections2(zipEntries, centralDir, eocd);\n      } catch (NoSuchAlgorithmException\n          | InvalidKeyException\n          | SignatureException\n          | ApkFormatException\n          | IOException e) {\n        throw new IOException(\"Failed to generate v2 signature\", e);\n      }\n\n      if (addV2SignatureRequest != null) {\n        apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();\n        if (sdkDependencyData != null) {\n          apkSigningBlock =\n              SigningBlockUtils.addToSigningBlock(\n                  apkSigningBlock, sdkDependencyData, DEPENDENCY_INFO_BLOCK_ID);\n        }\n        apkSigningBlock =\n            Bytes.concat(\n                new byte[addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock()],\n                apkSigningBlock);\n      } else {\n        apkSigningBlock = new byte[0];\n        if (sdkDependencyData != null) {\n          apkSigningBlock =\n              SigningBlockUtils.addToSigningBlock(\n                  apkSigningBlock, sdkDependencyData, DEPENDENCY_INFO_BLOCK_ID);\n          int paddingSize =\n              ApkSigningBlockUtils.generateApkSigningBlockPadding(\n                      zipEntries, /* apkSigningBlockPaddingSupported */ true)\n                  .getSecond();\n          apkSigningBlock = Bytes.concat(new byte[paddingSize], apkSigningBlock);\n        }\n      }\n      cachedApkSigningBlock = apkSigningBlock;\n    }\n\n    // Insert the APK Signing Block into the output right before the ZIP Central Directory and\n    // accordingly update the start offset of ZIP Central Directory in ZIP End of Central\n    // Directory.\n    zFile.directWrite(\n        zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(), apkSigningBlock);\n    zFile.setExtraDirectoryOffset(apkSigningBlock.length);\n\n    if (addV2SignatureRequest != null) {\n      addV2SignatureRequest.done();\n    }\n  }\n\n  private void onOutputClosed() {\n    if (!dirty) {\n      return;\n    }\n    signer.outputDone();\n    dirty = false;\n  }\n\n  private void setDirty() {\n    dirty = true;\n    cachedApkSigningBlock = null;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/SigningOptions.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.sign;\n\nimport com.android.apksig.util.RunnablesExecutor;\nimport com.google.auto.value.AutoValue;\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableList;\nimport java.security.PrivateKey;\nimport java.security.cert.X509Certificate;\nimport javax.annotation.Nonnull;\nimport javax.annotation.Nullable;\n\n/** A class that contains data to initialize SigningExtension. */\n@AutoValue\npublic abstract class SigningOptions {\n\n    /** An implementation of builder pattern to create a {@link SigningOptions} object. */\n    @AutoValue.Builder\n    public abstract static class Builder {\n        public abstract Builder setKey(@Nonnull PrivateKey key);\n        public abstract Builder setCertificates(@Nonnull ImmutableList<X509Certificate> certs);\n        public abstract Builder setCertificates(X509Certificate... certs);\n        public abstract Builder setV1SigningEnabled(boolean enabled);\n        public abstract Builder setV2SigningEnabled(boolean enabled);\n        public abstract Builder setMinSdkVersion(int version);\n        public abstract Builder setValidation(@Nonnull Validation validation);\n        public abstract Builder setExecutor(@Nullable RunnablesExecutor executor);\n        public abstract Builder setSdkDependencyData(@Nullable byte[] sdkDependencyData);\n\n        abstract SigningOptions autoBuild();\n\n        public SigningOptions build() {\n            SigningOptions options = autoBuild();\n            Preconditions.checkArgument(options.getMinSdkVersion() >= 0, \"minSdkVersion < 0\");\n            Preconditions.checkArgument(\n                    !options.getCertificates().isEmpty(),\n                    \"There should be at least one certificate in SigningOptions\");\n            return options;\n        }\n    }\n\n    public static Builder builder() {\n        return new AutoValue_SigningOptions.Builder()\n                .setV1SigningEnabled(false)\n                .setV2SigningEnabled(false)\n                .setValidation(Validation.ALWAYS_VALIDATE);\n    }\n\n    /** {@link PrivateKey} used to sign the archive. */\n    public abstract PrivateKey getKey();\n\n    /**\n     * A list of the {@link X509Certificate}s to embed in the signed APKs. The first\n     * element of the list must be the certificate associated with the private key.\n     */\n    public abstract ImmutableList<X509Certificate> getCertificates();\n\n    /** Shows whether signing with JAR Signature Scheme (aka v1 signing) is enabled. */\n    public abstract boolean isV1SigningEnabled();\n\n    /** Shows whether signing with APK Signature Scheme v2 (aka v2 signing) is enabled. */\n    public abstract boolean isV2SigningEnabled();\n\n    /** Minimum SDK version supported. */\n    public abstract int getMinSdkVersion();\n\n    /** Strategy of package signature validation */\n    public abstract Validation getValidation();\n\n    @Nullable\n    public abstract RunnablesExecutor getExecutor();\n\n  /** SDK dependencies of the APK */\n  @SuppressWarnings(\"mutable\")\n  @Nullable\n  public abstract byte[] getSdkDependencyData();\n\n    public enum Validation {\n        /** Always perform signature validation */\n        ALWAYS_VALIDATE,\n        /**\n         * Assume the signature is valid without validation i.e. don't resign if no files changed\n         */\n        ASSUME_VALID,\n        /** Assume the signature is invalid without validation i.e. unconditionally resign */\n        ASSUME_INVALID,\n    }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/sign/package-info.java",
    "content": "/*\n * Copyright (C) 2016 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\n/**\n * The {@code sign} package provides extensions for the {@code zip} package that allow:\n *\n * <ul>\n *   <li>Adding a {@code MANIFEST.MF} file to a zip making a jar.\n *   <li>Signing a jar.\n *   <li>Fully signing a jar using v2 apk signature.\n * </ul>\n *\n * <p>Because the {@code zip} package is completely independent of the {@code sign} package, the\n * actual coordination between the two is complex. The {@code sign} package works by registering\n * extensions with the {@code zip} package. These extensions are notified in changes made in the zip\n * and will change the zip file itself.\n *\n * <p>The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will ensure the zip\n * has a manifest file and is, therefore, a valid jar. The {@link\n * com.android.apkzlib.sign.SigningExtension} extension will ensure the jar is signed.\n *\n * <p>The extension mechanism used is the one provided in the {@code zip} package (see {@link\n * com.android.apkzlib.zip.ZFile} and {@link com.android.apkzlib.zip.ZFileExtension}. Building the\n * zip and then operating the extensions is not done sequentially, as we don't want to build a zip\n * and then sign it. We want to build a zip that is automatically signed. Extension are basically\n * observers that register on the zip and are notified when things happen in the zip. They will then\n * modify the zip accordingly.\n *\n * <p>The zip file notifies extensions in 4 critical moments: when a file is added or removed from\n * the zip, when the zip is about to be flushed to disk and when the zip's entries have been flushed\n * but the central directory not. At these moments, the extensions can act to update the zip in any\n * way they need.\n *\n * <p>To see how this works, consider the manifest generation extension: when the extension is\n * created, it checks the zip file to see if there is a manifest. If a manifest exists and does not\n * need updating, it does not change anything, otherwise it generates a new manifest for the zip\n * file. At this point, the extension could write the manifest to the zip, but we opted not to. It\n * would be irrelevant anyway as the zip will only be written when flushed.\n *\n * <p>Now, when the {@code ZFile} notifies the extension that it is about to start writing the zip\n * file, the manifest extension, if it has noted that the manifest needs to be rewritten, will --\n * before the {@code ZFile} actually writes anything -- modify the zip and add or replace the\n * existing manifest file. So, process-wise, the zip is written only once with the correct manifest.\n * The flow is as follows (if only the manifest generation extension was added to the {@code\n * ZFile}):\n *\n * <ol>\n *   <li>{@code ZFile.update()} is called.\n *   <li>{@code ZFile} calls {@code beforeUpdate()} for all {@code ZFileExtensions} registered, in\n *       this case, only the instance of the anonymous inner class generated in the {@code\n *       ManifestGenerationExtension} constructor is invoked.\n *   <li>{@code ManifestGenerationExtension.updateManifest()} is called.\n *   <li>If the manifest does not need to be updated, {@code updateManifest()} returns immediately.\n *   <li>If the manifest needs updating, {@code ZFile.add()} is invoked to add or replace the\n *       manifest.\n *   <li>{@code ManifestGenerationExtension.updateManifest()} returns.\n *   <li>{@code ZFile.update()} continues and writes the zip file, containing the manifest.\n *   <li>The zip is finally written with an updated manifest.\n * </ol>\n *\n * <p>To generate a signed apk, we need to add a second extension, the {@code SigningExtension}.\n * This extension will also register listeners with the {@code ZFile}.\n *\n * <p>In this case the flow would be (starting a bit earlier for clarity and assuming a package task\n * in the build process):\n *\n * <ol>\n *   <li>Package task creates a {@code ZFile} on the target apk (or non-existing file, if there is\n *       no target apk in the output directory).\n *   <li>Package task configures the {@code ZFile} with alignment rules.\n *   <li>Package task creates a {@code ManifestGenerationExtension}.\n *   <li>Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.\n *   <li>The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid\n *       manifest. No changes are done to the {@code ZFile}.\n *   <li>Package task creates a {@code SigningExtension}.\n *   <li>Package task registers the {@code SigningExtension} with the {@code ZFile}.\n *   <li>The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile} and\n *       look at the {@code ZFile} to see if there is a valid signature file.\n *   <li>If there are changes to the digital signature file needed, these are marked internally in\n *       the extension. If there are changes needed to the digests, the manifest is updated (by\n *       calling {@code ManifestGenerationExtension}.<br>\n *       <em>(note that this point, the apk file, if any existed, has not been touched, the manifest\n *       is only updated in memory and the digests of all files in the apk, if any, have been\n *       computed and stored in memory only; the digital signature of the {@code SF} file has not\n *       been computed.) </em>\n *   <li>The Package task now adds all files to the {@code ZFile}.\n *   <li>For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added}\n *       method of all registered extensions.\n *   <li>The {@code ManifestGenerationExtension} ignores added invocations.\n *   <li>The {@code SigningExtension} computes the digest for the added file and stores them in the\n *       manifest.<br>\n *       <em>(when all files are added to the apk, all digests are computed and the manifest is\n *       updated but only in memory; the apk file has not been touched; also note that {@code ZFile}\n *       has not actually written anything to disk at this point, all files added are kept in\n *       memory).</em>\n *   <li>Package task calls {@code ZFile.update()} to update the apk.\n *   <li>{@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is\n *       done before anything is written. In this case both the {@code ManifestGenerationExtension}\n *       and {@code SigningExtension} are invoked.\n *   <li>The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new\n *       manifest, unless nothing has changed, in which case it does nothing.\n *   <li>The {@code SigningExtension} will add the SF file (unless nothing has changed), will\n *       compute the digital signature of the SF file and write it to the {@code ZFile}.<br>\n *       <em>(note that the order by which the {@code ManifestGenerationExtension} and {@code\n *       SigningExtension} are called is non-deterministic; however, this is not a problem because\n *       the manifest is already computed by the {@code ManifestGenerationExtension} at this time\n *       and the {@code SigningExtension} will obtain the manifest data from the {@code\n *       ManifestGenerationExtension} and not from the {@code ZFile}; this means that the {@code SF}\n *       file may be added to the {@code ZFile} before the {@code MF} file, but that is\n *       irrelevant.)</em>\n *   <li>Once both extensions have finished doing the {@code beforeUpdate()} method, the {@code\n *       ZFile.update()} method continues.\n *   <li>{@code ZFile.update()} writes all changes and new entries to the zip file.\n *   <li>{@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all registered\n *       extensions. {@code SigningExtension} will kick in at this point, if v2 signature has\n *       changed.\n *   <li>{@code ZFile} writes the central directory and EOCD.\n *   <li>{@code ZFile.update()} returns control to the package task.\n *   <li>The package task finishes.\n * </ol>\n *\n * <em>(*) There is a number of optimizations if we're adding files from another {@code ZFile},\n * which is the case when we add the output of aapt to the apk. In particular, files from the aapt\n * are ignored if they are already in the apk (same name, same CRC32) and also files copied from the\n * aapt's output are not recompressed (the binary compressed data is directly copied to the\n * zip).</em>\n *\n * <p>If there are no changes to the {@code ZFile} made by the package task and the file's manifest\n * and v1 signatures are correct, neither the {@code ManifestGenerationExtension} nor the {@code\n * SigningExtension} will not do anything on the {@code beforeUpdate()} and the {@code ZFile} won't\n * even be open for writing.\n *\n * <p>This implementation provides perfect incremental updates.\n *\n * <p>Additionally, by adding/removing extensions we can configure what type of apk we want:\n *\n * <ul>\n *   <li>No SigningExtension => Aligned, unsigned apk.\n *   <li>SigningExtension => Aligned, signed apk.\n * </ul>\n *\n * So, by configuring which extensions to add, the package task can decide what type of apk we want.\n */\npackage com.android.apkzlib.sign;\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/ApkZLibPair.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\n/** Pair implementation to use with the {@code apkzlib} library. */\npublic class ApkZLibPair<T1, T2> {\n\n  /** First value. */\n  public T1 v1;\n\n  /** Second value. */\n  public T2 v2;\n\n  /**\n   * Creates a new pair.\n   *\n   * @param v1 the first value\n   * @param v2 the second value\n   */\n  public ApkZLibPair(T1 v1, T2 v2) {\n    this.v1 = v1;\n    this.v2 = v2;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/CachedFileContents.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport com.google.common.base.Objects;\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.Hashing;\nimport com.google.common.io.Files;\nimport java.io.File;\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/**\n * A cache for file contents. The cache allows closing a file and saving in memory its contents (or\n * some related information). It can then be used to check if the contents are still valid at some\n * later time. Typical usage flow is:\n *\n * <p>\n *\n * <pre>{@code\n * Object fileRepresentation = // ...\n * File toWrite = // ...\n * // Write file contents and update in memory representation\n * CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);\n * contents.closed(fileRepresentation);\n *\n * // Later, when data is needed:\n * if (contents.isValid()) {\n *     fileRepresentation = contents.getCache();\n * } else {\n *     // Re-read the file and recreate the file representation\n * }\n * }</pre>\n *\n * @param <T> the type of cached contents\n */\npublic class CachedFileContents<T> {\n\n  /** The file. */\n  private final File file;\n\n  /** Time when last closed (time when {@link #closed(Object)} was invoked). */\n  private long lastClosed;\n\n  /** Size of the file when last closed. */\n  private long size;\n\n  /** Hash of the file when closed. {@code null} if hashing failed for some reason. */\n  @Nullable private HashCode hash;\n\n  /** Cached data associated with the file. */\n  @Nullable private T cache;\n\n  /**\n   * Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked to\n   * set the cache.\n   *\n   * @param file the file\n   */\n  public CachedFileContents(File file) {\n    this.file = file;\n  }\n\n  /**\n   * Should be called when the file's contents are set and the file closed. This will save the cache\n   * and register the file's timestamp to later detect if it has been modified.\n   *\n   * <p>This method can be called as many times as the file has been written.\n   *\n   * @param cache an optional cache to save\n   */\n  public void closed(@Nullable T cache) {\n    this.cache = cache;\n    lastClosed = file.lastModified();\n    size = file.length();\n    hash = hashFile();\n  }\n\n  /**\n   * Are the cached contents still valid? If this method determines that the file has been modified\n   * since the last time {@link #closed(Object)} was invoked.\n   *\n   * @return are the cached contents still valid? If this method returns {@code false}, the cache is\n   *     cleared\n   */\n  public boolean isValid() {\n    boolean valid = true;\n\n    if (!file.exists()) {\n      valid = false;\n    }\n\n    if (valid && file.lastModified() != lastClosed) {\n      valid = false;\n    }\n\n    if (valid && file.length() != size) {\n      valid = false;\n    }\n\n    if (valid && !Objects.equal(hash, hashFile())) {\n      valid = false;\n    }\n\n    if (!valid) {\n      cache = null;\n    }\n\n    return valid;\n  }\n\n  /**\n   * Obtains the cached data set with {@link #closed(Object)} if the file has not been modified\n   * since {@link #closed(Object)} was invoked.\n   *\n   * @return the last cached data or {@code null} if the file has been modified since {@link\n   *     #closed(Object)} has been invoked\n   */\n  @Nullable\n  public T getCache() {\n    return cache;\n  }\n\n  /**\n   * Computes the hashcode of the cached file.\n   *\n   * @return the hash code\n   */\n  @Nullable\n  private HashCode hashFile() {\n    try {\n      return Files.asByteSource(file).hash(Hashing.crc32());\n    } catch (IOException e) {\n      return null;\n    }\n  }\n\n  /**\n   * Obtains the file used for caching.\n   *\n   * @return the file; this file always exists and contains the old (cached) contents of the file\n   */\n  public File getFile() {\n    return file;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/CachedSupplier.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport com.google.common.base.Supplier;\n\n/**\n * Supplier that will cache a computed value and always supply the same value. It can be used to\n * lazily compute data. For example:\n *\n * <pre>{@code\n * CachedSupplier<Integer> value = new CachedSupplier<>(() -> {\n *     Integer result;\n *     // Do some expensive computation.\n *     return result;\n * });\n *\n * if (a) {\n *     // We need the result of the expensive computation.\n *     Integer r = value.get();\n * }\n *\n * if (b) {\n *     // We also need the result of the expensive computation.\n *     Integer r = value.get();\n * }\n *\n * // If neither a nor b are true, we avoid doing the computation at all.\n * }</pre>\n */\npublic class CachedSupplier<T> {\n\n  /**\n   * The cached data, {@code null} if computation resulted in {@code null}. It is also {@code null}\n   * if the cached data has not yet been computed.\n   */\n  private T cached;\n\n  /** Is the current data in {@link #cached} valid? */\n  private boolean valid;\n\n  /** Actual supplier of data, if computation is needed. */\n  private final Supplier<T> supplier;\n\n  /** Creates a new supplier. */\n  public CachedSupplier(Supplier<T> supplier) {\n    valid = false;\n    this.supplier = supplier;\n  }\n\n  /**\n   * Obtains the value.\n   *\n   * @return the value, either cached (if one exists) or computed\n   */\n  public synchronized T get() {\n    if (!valid) {\n      cached = supplier.get();\n      valid = true;\n    }\n\n    return cached;\n  }\n\n  /**\n   * Resets the cache forcing a {@code get()} on the supplier next time {@link #get()} is invoked.\n   */\n  public synchronized void reset() {\n    cached = null;\n    valid = false;\n  }\n\n  /**\n   * In some cases, we may be able to precompute the cache value (or load it from somewhere we had\n   * previously stored it). This method allows the cache value to be loaded.\n   *\n   * <p>If this method is invoked, then an invocation of {@link #get()} will not trigger an\n   * invocation of the supplier provided in the constructor.\n   *\n   * @param t the new cache contents; will replace any currently cache content, if one exists\n   */\n  public synchronized void precomputed(T t) {\n    cached = t;\n    valid = true;\n  }\n\n  /**\n   * Checks if the contents of the cache are valid.\n   *\n   * @return are there valid contents in the cache?\n   */\n  public synchronized boolean isValid() {\n    return valid;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionConsumer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/** Consumer that can throw an {@link IOException}. */\npublic interface IOExceptionConsumer<T> {\n\n  /**\n   * Performs an operation on the given input.\n   *\n   * @param input the input\n   */\n  void accept(@Nullable T input) throws IOException;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionFunction.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport com.google.common.base.Function;\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/** Function that can throw an I/O Exception */\npublic interface IOExceptionFunction<F, T> {\n\n  /**\n   * Applies the function to the given input.\n   *\n   * @param input the input\n   * @return the function result\n   */\n  @Nullable\n  T apply(@Nullable F input) throws IOException;\n\n  /**\n   * Wraps a function that may throw an IO Exception throwing an {@link IOExceptionWrapper}.\n   *\n   * @param f the function\n   */\n  static <F, T> Function<F, T> asFunction(IOExceptionFunction<F, T> f) {\n    return i -> {\n      try {\n        return f.apply(i);\n      } catch (IOException e) {\n        throw new IOExceptionWrapper(e);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionRunnable.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport java.io.IOException;\n\n/** Runnable that can throw I/O exceptions. */\npublic interface IOExceptionRunnable {\n\n  /**\n   * Runs the runnable.\n   *\n   * @throws IOException failed to run\n   */\n  void run() throws IOException;\n\n  /**\n   * Wraps a runnable that may throw an IO Exception throwing an {@code UncheckedIOException}.\n   *\n   * @param r the runnable\n   */\n  static Runnable asRunnable(IOExceptionRunnable r) {\n    return () -> {\n      try {\n        r.run();\n      } catch (IOException e) {\n        throw new IOExceptionWrapper(e);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/IOExceptionWrapper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport java.io.IOException;\n\n/**\n * Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O\n * exceptions in functional interfaces that do not allow it and catching the exception afterwards.\n */\npublic class IOExceptionWrapper extends RuntimeException {\n\n  /**\n   * Creates a new exception.\n   *\n   * @param e the I/O exception to encapsulate\n   */\n  public IOExceptionWrapper(IOException e) {\n    super(e);\n  }\n\n  @Override\n  public IOException getCause() {\n    return (IOException) super.getCause();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/SigningBlockUtils.java",
    "content": "/*\n * Copyright (C) 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.utils;\n\nimport static java.nio.ByteOrder.LITTLE_ENDIAN;\n\nimport com.android.apksig.apk.ApkSigningBlockNotFoundException;\nimport com.android.apksig.apk.ApkUtils;\nimport com.android.apksig.apk.ApkUtils.ApkSigningBlock;\nimport com.android.apksig.internal.apk.ApkSigningBlockUtils;\nimport com.android.apksig.internal.util.Pair;\nimport com.android.apksig.util.DataSource;\nimport com.android.apksig.util.DataSources;\nimport com.android.apksig.zip.ZipFormatException;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.primitives.Ints;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport javax.annotation.Nullable;\n\n/** Generates and appends a new block to APK v2 Signature block. */\npublic final class SigningBlockUtils {\n\n  private static final int MAGIC_NUM_BYTES = 16;\n  private static final int BLOCK_LENGTH_NUM_BYTES = 8;\n  static final int SIZE_OF_BLOCK_NUM_BYTES = 8;\n  static final int BLOCK_ID_NUM_BYTES = 4;\n\n  static final int ANDROID_COMMON_PAGE_ALIGNMENT_NUM_BYTES = 4096;\n  static final int VERITY_PADDING_BLOCK_ID = 0x42726577;\n\n  /**\n   * Generates a new block with the given block value and block id, and appends it to the signing\n   * block.\n   *\n   * @param signingBlock Block containing v2 signature and (optionally) padding block or null.\n   * @param blockValue byte array containing block value of the new block or null.\n   * @param blockId block id of the new block.\n   * @return APK v2 block with signatures and the new block. If {@code blockValue} is null the\n   *     {@code signingBlock} is returned without any modification. If {@code signingBlock} is null,\n   *     a new signature block is created containing the new block and, optionally, padding block.\n   */\n  public static byte[] addToSigningBlock(byte[] signingBlock, byte[] blockValue, int blockId)\n      throws IOException {\n    if (blockValue == null || blockValue.length == 0) {\n      return signingBlock;\n    }\n    if (signingBlock == null || signingBlock.length == 0) {\n      return createSigningBlock(blockValue, blockId);\n    }\n    return appendToSigningBlock(signingBlock, blockValue, blockId);\n  }\n\n  /**\n   * Adds a new block to the signature block and a padding block, if required.\n   *\n   * @param signingBlock APK v2 signing block containing : length prefix, signers (can include\n   *     padding block), length postfix and APK sig v2 block magic.\n   * @param blockValue byte array containing block value of the new block.\n   * @param blockId block id of the new block.\n   * @return APK v2 signing block containing : length prefix, signers including the new block (may\n   *     include padding block as well), length postfix and APK sig v2 block magic.\n   */\n  private static byte[] appendToSigningBlock(byte[] signingBlock, byte[] blockValue, int blockId)\n      throws IOException {\n    ImmutableList<Pair<byte[], Integer>> entries =\n        ImmutableList.<Pair<byte[], Integer>>builder()\n            .addAll(extractAllSigners(DataSources.asDataSource(ByteBuffer.wrap(signingBlock))))\n            .add(Pair.of(blockValue, blockId))\n            .build();\n    return ApkSigningBlockUtils.generateApkSigningBlock(entries);\n  }\n\n  /**\n   * Generate APK sig v2 block containing a block composed of the provided block value and id, and\n   * (optionally) padding block.\n   */\n  private static byte[] createSigningBlock(byte[] blockValue, int blockId) {\n    return ApkSigningBlockUtils.generateApkSigningBlock(\n        ImmutableList.of(Pair.of(blockValue, blockId)));\n  }\n\n  /**\n   * Extracts all signing block entries except padding block.\n   *\n   * @param signingBlock APK v2 signing block containing: length prefix, signers (can include\n   *     padding block), length postfix and APK sig v2 block magic.\n   * @return list of block entry value and block entry id pairs.\n   */\n  private static ImmutableList<Pair<byte[], Integer>> extractAllSigners(DataSource signingBlock)\n      throws IOException {\n    long wholeBlockSize = signingBlock.size();\n    // Take the segment of the existing signing block without the length prefix (8 bytes)\n    // at the beginning and the length and magic (24 bytes) at the end, so it is just the sequence\n    // of length prefix id value pairs.\n    DataSource lengthPrefixedIdValuePairsSource =\n        signingBlock.slice(\n            SIZE_OF_BLOCK_NUM_BYTES,\n            wholeBlockSize - 2 * SIZE_OF_BLOCK_NUM_BYTES - MAGIC_NUM_BYTES);\n    final int lengthAndIdByteCount = BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES;\n    ByteBuffer lengthAndId = ByteBuffer.allocate(lengthAndIdByteCount).order(LITTLE_ENDIAN);\n    ImmutableList.Builder<Pair<byte[], Integer>> idValuePairs = ImmutableList.builder();\n\n    for (int index = 0; index <= lengthPrefixedIdValuePairsSource.size() - lengthAndIdByteCount; ) {\n      lengthPrefixedIdValuePairsSource.copyTo(index, lengthAndIdByteCount, lengthAndId);\n      lengthAndId.flip();\n      int blockLength = Ints.checkedCast(lengthAndId.getLong());\n      int id = lengthAndId.getInt();\n      lengthAndId.clear();\n\n      if (id != VERITY_PADDING_BLOCK_ID) {\n        int blockValueSize = blockLength - BLOCK_ID_NUM_BYTES;\n        ByteBuffer blockValue = ByteBuffer.allocate(blockValueSize);\n        lengthPrefixedIdValuePairsSource.copyTo(\n            index + BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES, blockValueSize, blockValue);\n        idValuePairs.add(Pair.of(blockValue.array(), id));\n      }\n\n      index += blockLength + BLOCK_LENGTH_NUM_BYTES;\n    }\n    return idValuePairs.build();\n  }\n\n  /**\n   * Extract a block with the given id from the APK. If there is more than one block with the same\n   * ID, the first block will be returned. If there are no block with the give id, {@code null} will\n   * be returned.\n   *\n   * @param apk APK file\n   * @param blockId id of the block to be extracted.\n   */\n  @Nullable\n  public static ByteBuffer extractBlock(File apk, int blockId)\n      throws IOException, ZipFormatException, ApkSigningBlockNotFoundException {\n    try (RandomAccessFile file = new RandomAccessFile(apk, \"r\")) {\n      DataSource apkDataSource = DataSources.asDataSource(file);\n      ApkSigningBlock signingBlockInfo =\n          ApkUtils.findApkSigningBlock(apkDataSource, ApkUtils.findZipSections(apkDataSource));\n\n      DataSource wholeV2Block = signingBlockInfo.getContents();\n      final int lengthAndIdByteCount = BLOCK_LENGTH_NUM_BYTES + BLOCK_ID_NUM_BYTES;\n      DataSource signingBlock =\n          wholeV2Block.slice(\n              SIZE_OF_BLOCK_NUM_BYTES,\n              wholeV2Block.size() - SIZE_OF_BLOCK_NUM_BYTES - MAGIC_NUM_BYTES);\n      ByteBuffer lengthAndId =\n          ByteBuffer.allocate(lengthAndIdByteCount).order(ByteOrder.LITTLE_ENDIAN);\n      for (int index = 0; index <= signingBlock.size() - lengthAndIdByteCount; ) {\n        signingBlock.copyTo(index, lengthAndIdByteCount, lengthAndId);\n        lengthAndId.flip();\n        int blockLength = (int) lengthAndId.getLong();\n        int id = lengthAndId.getInt();\n        lengthAndId.flip();\n        if (id == blockId) {\n          ByteBuffer block = ByteBuffer.allocate(blockLength - BLOCK_ID_NUM_BYTES);\n          signingBlock.copyTo(\n              index + lengthAndIdByteCount, blockLength - BLOCK_ID_NUM_BYTES, block);\n          block.flip();\n          return block;\n        }\n        index += blockLength + BLOCK_LENGTH_NUM_BYTES;\n      }\n      return null;\n    }\n  }\n\n  private SigningBlockUtils() {}\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/utils/package-info.java",
    "content": "/*\n * Copyright (C) 2017 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\n/** Utilities to work with {@code apkzlib}. */\npackage com.android.tools.build.apkzlib.utils;\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreator.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\nimport com.google.common.base.Function;\nimport com.google.common.base.Predicate;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/** Creates or updates APKs based on provided entries. */\npublic interface ApkCreator extends Closeable {\n\n  /**\n   * Copies the content of a Jar/Zip archive into the receiver archive.\n   *\n   * <p>An optional predicate allows to selectively choose which files to copy over and an option\n   * function allows renaming the files as they are copied.\n   *\n   * @param zip the zip to copy data from\n   * @param transform an optional transform to apply to file names before copying them\n   * @param isIgnored an optional filter or {@code null} to mark which out files should not be\n   *     added, even through they are on the zip; if {@code transform} is specified, then this\n   *     predicate applies after transformation\n   * @throws IOException I/O error\n   */\n  void writeZip(\n      File zip, @Nullable Function<String, String> transform, @Nullable Predicate<String> isIgnored)\n      throws IOException;\n\n  /**\n   * Writes a new {@link File} into the archive. If a file already existed with the given path, it\n   * should be replaced.\n   *\n   * @param inputFile the {@link File} to write.\n   * @param apkPath the filepath inside the archive.\n   * @throws IOException I/O error\n   */\n  void writeFile(File inputFile, String apkPath) throws IOException;\n\n  /**\n   * Deletes a file in a given path.\n   *\n   * @param apkPath the path to remove\n   * @throws IOException failed to remove the entry\n   */\n  void deleteFile(String apkPath) throws IOException;\n\n  /** Returns true if the APK will be rewritten on close. */\n  boolean hasPendingChangesWithWait() throws IOException;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkCreatorFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\nimport com.android.tools.build.apkzlib.sign.SigningOptions;\nimport com.google.auto.value.AutoValue;\nimport com.google.common.base.Optional;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Predicate;\nimport java.io.File;\nimport javax.annotation.Nonnull;\nimport javax.annotation.Nullable;\n\n/** Factory that creates instances of {@link ApkCreator}. */\npublic interface ApkCreatorFactory {\n\n  /**\n   * Creates an {@link ApkCreator} with a given output location, and signing information.\n   *\n   * @param creationData the information to create the APK\n   */\n  ApkCreator make(CreationData creationData);\n\n  /**\n   * Data structure with the required information to initiate the creation of an APK. See {@link\n   * ApkCreatorFactory#make(CreationData)}.\n   */\n  @AutoValue\n  abstract class CreationData {\n\n    /** An implementation of builder pattern to create a {@link CreationData} object. */\n    @AutoValue.Builder\n    public abstract static class Builder {\n      public abstract Builder setApkPath(@Nonnull File apkPath);\n\n      public abstract Builder setSigningOptions(@Nonnull SigningOptions signingOptions);\n\n      public abstract Builder setBuiltBy(@Nullable String buildBy);\n\n      public abstract Builder setCreatedBy(@Nullable String createdBy);\n\n      public abstract Builder setNativeLibrariesPackagingMode(\n          NativeLibrariesPackagingMode packagingMode);\n\n      public abstract Builder setNoCompressPredicate(Predicate<String> predicate);\n\n      public abstract Builder setIncremental(boolean incremental);\n\n      abstract CreationData autoBuild();\n\n      public CreationData build() {\n        CreationData data = autoBuild();\n        Preconditions.checkArgument(data.getApkPath() != null, \"Output apk path is not set\");\n        return data;\n      }\n    }\n\n    public static Builder builder() {\n      return new AutoValue_ApkCreatorFactory_CreationData.Builder()\n          .setBuiltBy(null)\n          .setCreatedBy(null)\n          .setNoCompressPredicate(s -> false)\n          .setIncremental(false);\n    }\n\n    /**\n     * Obtains the path where the APK should be located. If the path already exists, then the APK\n     * may be updated instead of re-created.\n     *\n     * @return the path that may already exist or not\n     */\n    public abstract File getApkPath();\n\n    /**\n     * Obtains the data used to sign the APK.\n     *\n     * @return the SigningOptions\n     */\n    @Nonnull\n    public abstract Optional<SigningOptions> getSigningOptions();\n\n    /**\n     * Obtains the \"built-by\" text for the APK.\n     *\n     * @return the text or {@code null} if the default should be used\n     */\n    @Nullable\n    public abstract String getBuiltBy();\n\n    /**\n     * Obtains the \"created-by\" text for the APK.\n     *\n     * @return the text or {@code null} if the default should be used\n     */\n    @Nullable\n    public abstract String getCreatedBy();\n\n    /** Returns the packaging policy that the {@link ApkCreator} should use for native libraries. */\n    public abstract NativeLibrariesPackagingMode getNativeLibrariesPackagingMode();\n\n    /** Returns the predicate to decide which file paths should be uncompressed. */\n    public abstract Predicate<String> getNoCompressPredicate();\n\n    /**\n     * Returns if this apk build is incremental.\n     *\n     * As mentioned in {@link getApkPath} description, we may already have an existing apk in place.\n     * This is the case when e.g. building APK via build system and this is not the first build.\n     * In that case the build is called incremental and internal APK data might be reused speeding\n     * the build up.\n     */\n    public abstract boolean isIncremental();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreator.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\nimport com.android.tools.build.apkzlib.zip.AlignmentRule;\nimport com.android.tools.build.apkzlib.zip.AlignmentRules;\nimport com.android.tools.build.apkzlib.zip.StoredEntry;\nimport com.android.tools.build.apkzlib.zip.ZFile;\nimport com.android.tools.build.apkzlib.zip.ZFileOptions;\nimport com.google.common.base.Function;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Predicate;\nimport com.google.common.io.Closer;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport javax.annotation.Nullable;\n\n/** {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK. */\nclass ApkZFileCreator implements ApkCreator {\n\n  /** Suffix for native libraries. */\n  private static final String NATIVE_LIBRARIES_SUFFIX = \".so\";\n\n  /** Shared libraries are alignment at 4096 boundaries. */\n  private static final AlignmentRule SO_RULE =\n      AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096);\n\n  /** The zip file. */\n  private final ZFile zip;\n\n  /** Has the zip file been closed? */\n  private boolean closed;\n\n  /** Predicate defining which files should not be compressed. */\n  private final Predicate<String> noCompressPredicate;\n\n  /**\n   * Creates a new creator.\n   *\n   * @param creationData the data needed to create the APK\n   * @param options zip file options\n   * @throws IOException failed to create the zip\n   */\n  ApkZFileCreator(ApkCreatorFactory.CreationData creationData, ZFileOptions options)\n      throws IOException {\n\n    switch (creationData.getNativeLibrariesPackagingMode()) {\n      case COMPRESSED:\n        noCompressPredicate = creationData.getNoCompressPredicate();\n        break;\n      case UNCOMPRESSED_AND_ALIGNED:\n        Predicate<String> baseNoCompressPredicate = creationData.getNoCompressPredicate();\n        noCompressPredicate =\n            name -> baseNoCompressPredicate.apply(name) || name.endsWith(NATIVE_LIBRARIES_SUFFIX);\n        options.setAlignmentRule(AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));\n        break;\n      default:\n        throw new AssertionError();\n    }\n    // In case of incremental build we can skip validation since we generated the previous apk and\n    // we trust ourselves\n    options.setSkipValidation(creationData.isIncremental());\n\n    zip =\n        ZFiles.apk(\n            creationData.getApkPath(),\n            options,\n            creationData.getSigningOptions(),\n            creationData.getBuiltBy(),\n            creationData.getCreatedBy());\n    closed = false;\n  }\n\n  @Override\n  public void writeZip(\n      File zip, @Nullable Function<String, String> transform, @Nullable Predicate<String> isIgnored)\n      throws IOException {\n    Preconditions.checkState(!closed, \"closed == true\");\n    Preconditions.checkArgument(zip.isFile(), \"!zip.isFile()\");\n\n    Closer closer = Closer.create();\n    try {\n      ZFile toMerge = closer.register(ZFile.openReadWrite(zip));\n\n      Predicate<String> ignorePredicate;\n      if (isIgnored == null) {\n        ignorePredicate = s -> false;\n      } else {\n        ignorePredicate = isIgnored;\n      }\n\n      // Files that *must* be uncompressed in the result should not be merged and should be\n      // added after. This is just very slightly less efficient than ignoring just the ones\n      // that were compressed and must be uncompressed, but it is a lot simpler :)\n      Predicate<String> noMergePredicate =\n          v -> ignorePredicate.apply(v) || noCompressPredicate.apply(v);\n\n      this.zip.mergeFrom(toMerge, noMergePredicate);\n\n      for (StoredEntry toMergeEntry : toMerge.entries()) {\n        String path = toMergeEntry.getCentralDirectoryHeader().getName();\n        if (noCompressPredicate.apply(path) && !ignorePredicate.apply(path)) {\n          // This entry *must* be uncompressed so it was ignored in the merge and should\n          // now be added to the apk.\n          try (InputStream ignoredData = toMergeEntry.open()) {\n            this.zip.add(path, ignoredData, false);\n          }\n        }\n      }\n    } catch (Throwable t) {\n      throw closer.rethrow(t);\n    } finally {\n      closer.close();\n    }\n  }\n\n  @Override\n  public void writeFile(File inputFile, String apkPath) throws IOException {\n    Preconditions.checkState(!closed, \"closed == true\");\n\n    boolean mayCompress = !noCompressPredicate.apply(apkPath);\n\n    Closer closer = Closer.create();\n    try {\n      FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile));\n      zip.add(apkPath, inputFileStream, mayCompress);\n    } catch (IOException e) {\n      throw closer.rethrow(e, IOException.class);\n    } catch (Throwable t) {\n      throw closer.rethrow(t);\n    } finally {\n      closer.close();\n    }\n  }\n\n  @Override\n  public void deleteFile(String apkPath) throws IOException {\n    Preconditions.checkState(!closed, \"closed == true\");\n\n    StoredEntry entry = zip.get(apkPath);\n    if (entry != null) {\n      entry.delete();\n    }\n  }\n\n  @Override\n  public boolean hasPendingChangesWithWait() throws IOException {\n    return zip.hasPendingChangesWithWait();\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (closed) {\n      return;\n    }\n\n    zip.close();\n    closed = true;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ApkZFileCreatorFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.android.tools.build.apkzlib.zip.ZFileOptions;\nimport java.io.IOException;\n\n/** Creates instances of {@link ApkZFileCreator}. */\npublic class ApkZFileCreatorFactory implements ApkCreatorFactory {\n\n  /** Options for the {@link ZFileOptions} to use in all APKs. */\n  private final ZFileOptions options;\n\n  /**\n   * Creates a new factory.\n   *\n   * @param options the options to use for all instances created\n   */\n  public ApkZFileCreatorFactory(ZFileOptions options) {\n    this.options = options;\n  }\n\n  @Override\n  public ApkCreator make(CreationData creationData) {\n    try {\n      return new ApkZFileCreator(creationData, options);\n    } catch (IOException e) {\n      throw new IOExceptionWrapper(e);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ManifestAttributes.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\n/** Java manifest attributes and some default values. */\npublic interface ManifestAttributes {\n  /** Manifest attribute with the built by information. */\n  String BUILT_BY = \"Built-By\";\n\n  /** Manifest attribute with the created by information. */\n  String CREATED_BY = \"Created-By\";\n\n  /** Manifest attribute with the manifest version. */\n  String MANIFEST_VERSION = \"Manifest-Version\";\n\n  /** Manifest attribute value with the manifest version. */\n  String CURRENT_MANIFEST_VERSION = \"1.0\";\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/NativeLibrariesPackagingMode.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\n/** Describes how native libs should be packaged. */\npublic enum NativeLibrariesPackagingMode {\n  /** Native libs are packaged as any other file. */\n  COMPRESSED,\n\n  /**\n   * Native libs are packaged uncompressed and page-aligned, so they can be mapped into memory at\n   * runtime.\n   *\n   * <p>Support for this mode was added in Android 23, it only works if the {@code\n   * extractNativeLibs} attribute is set in the manifest.\n   */\n  UNCOMPRESSED_AND_ALIGNED;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/ZFiles.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zfile;\n\nimport com.android.tools.build.apkzlib.sign.ManifestGenerationExtension;\nimport com.android.tools.build.apkzlib.sign.SigningExtension;\nimport com.android.tools.build.apkzlib.sign.SigningOptions;\nimport com.android.tools.build.apkzlib.zip.AlignmentRule;\nimport com.android.tools.build.apkzlib.zip.AlignmentRules;\nimport com.android.tools.build.apkzlib.zip.ZFile;\nimport com.android.tools.build.apkzlib.zip.ZFileOptions;\nimport com.google.common.base.Optional;\nimport java.io.File;\nimport java.io.IOException;\nimport java.security.InvalidKeyException;\nimport java.security.NoSuchAlgorithmException;\nimport javax.annotation.Nullable;\n\n/** Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ... */\npublic class ZFiles {\n\n  /** By default all non-compressed files are alignment at 4 byte boundaries.. */\n  private static final AlignmentRule APK_DEFAULT_RULE = AlignmentRules.constant(4);\n\n  /** Default build by string. */\n  private static final String DEFAULT_BUILD_BY = \"Generated-by-ADT\";\n\n  /** Default created by string. */\n  private static final String DEFAULT_CREATED_BY = \"Generated-by-ADT\";\n\n  /**\n   * Creates a new zip file configured as an apk, based on a given file.\n   *\n   * @param f the file, if this path does not represent an existing path, will create a {@link\n   *     ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is\n   *     invoked)\n   * @param options the options to create the {@link ZFile}\n   * @return the zip file\n   * @throws IOException failed to create the zip file\n   */\n  public static ZFile apk(File f, ZFileOptions options) throws IOException {\n    options.setAlignmentRule(AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE));\n    return ZFile.openReadWrite(f, options);\n  }\n\n  /**\n   * Creates a new zip file configured as an apk, based on a given file.\n   *\n   * @param f the file, if this path does not represent an existing path, will create a {@link\n   *     ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is\n   *     invoked)\n   * @param options the options to create the {@link ZFile}\n   * @param signingOptions the options to sign the apk\n   * @param builtBy who to mark as builder in the manifest\n   * @param createdBy who to mark as creator in the manifest\n   * @return the zip file\n   * @throws IOException failed to create the zip file\n   */\n  public static ZFile apk(\n      File f,\n      ZFileOptions options,\n      Optional<SigningOptions> signingOptions,\n      @Nullable String builtBy,\n      @Nullable String createdBy)\n      throws IOException {\n    return apk(\n        f, options, signingOptions, builtBy, createdBy, options.getAlwaysGenerateJarManifest());\n  }\n\n  /**\n   * Creates a new zip file configured as an apk, based on a given file.\n   *\n   * @param f the file, if this path does not represent an existing path, will create a {@link\n   *     ZFile} based on an non-existing path (a zip will be created when {@link ZFile#close()} is\n   *     invoked)\n   * @param options the options to create the {@link ZFile}\n   * @param signingOptions the options to sign the apk\n   * @param builtBy who to mark as builder in the manifest\n   * @param createdBy who to mark as creator in the manifest\n   * @param writeManifest a migration parameter that forces keeping (useless) manifest.mf file in\n   *     apk file in order to prevent breaking changes. Clients of the previous interface will still\n   *     get apk with manifest.mf because the flag is true by default\n   * @return the zip file\n   * @throws IOException failed to create the zip file\n   * @deprecated Use ZFileOptions.setAlwaysGenerateJarManifest() instead.\n   */\n  @Deprecated\n  // This method can be removed once ZFileOptions.getAlwaysGenerateJarManifest() is on Maven.\n  public static ZFile apk(\n      File f,\n      ZFileOptions options,\n      Optional<SigningOptions> signingOptions,\n      @Nullable String builtBy,\n      @Nullable String createdBy,\n      boolean writeManifest)\n      throws IOException {\n    ZFile zfile = apk(f, options);\n\n    if ((signingOptions.isPresent() && signingOptions.get().isV1SigningEnabled())\n        || writeManifest) {\n      if (builtBy == null) {\n        builtBy = DEFAULT_BUILD_BY;\n      }\n\n      if (createdBy == null) {\n        createdBy = DEFAULT_CREATED_BY;\n      }\n      ManifestGenerationExtension manifestExt = new ManifestGenerationExtension(builtBy, createdBy);\n      manifestExt.register(zfile);\n    }\n\n    if (signingOptions.isPresent()) {\n      SigningOptions signOptions = signingOptions.get();\n      try {\n        new SigningExtension(signOptions).register(zfile);\n      } catch (NoSuchAlgorithmException | InvalidKeyException e) {\n        throw new IOException(\"Failed to create signature extensions\", e);\n      }\n    }\n\n    return zfile;\n  }\n\n  private ZFiles() {}\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zfile/package-info.java",
    "content": "/*\n * Copyright (C) 2017 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\n/** The {@code zfile} package contains */\npackage com.android.tools.build.apkzlib.zfile;\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRule.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n\n/** An alignment rule defines how to a file should be aligned in a zip, based on its name. */\npublic interface AlignmentRule {\n\n  /** Alignment value of files that do not require alignment. */\n  int NO_ALIGNMENT = 1;\n\n  /**\n   * Obtains the alignment this rule computes for a given path.\n   *\n   * @param path the path in the zip file\n   * @return the alignment value, always greater than {@code 0}; if this rule places no restrictions\n   *     on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned\n   */\n  int alignment(String path);\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/AlignmentRules.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.google.common.base.Preconditions;\n\n/** Factory for instances of {@link AlignmentRule}. */\npublic final class AlignmentRules {\n\n  private AlignmentRules() {}\n\n  /**\n   * A rule that defines a constant alignment for all files.\n   *\n   * @param alignment the alignment\n   * @return the rule\n   */\n  public static AlignmentRule constant(int alignment) {\n    Preconditions.checkArgument(alignment > 0, \"alignment <= 0\");\n\n    return (String path) -> alignment;\n  }\n\n  /**\n   * A rule that defines constant alignment for all files with a certain suffix, placing no\n   * restrictions on other files.\n   *\n   * @param suffix the suffix\n   * @param alignment the alignment for paths that match the provided suffix\n   * @return the rule\n   */\n  public static AlignmentRule constantForSuffix(String suffix, int alignment) {\n    Preconditions.checkArgument(!suffix.isEmpty(), \"suffix.isEmpty()\");\n    Preconditions.checkArgument(alignment > 0, \"alignment <= 0\");\n\n    return (String path) -> path.endsWith(suffix) ? alignment : AlignmentRule.NO_ALIGNMENT;\n  }\n\n  /**\n   * A rule that applies other rules in order.\n   *\n   * @param rules all rules to be tried; the first rule that does not return {@link\n   *     AlignmentRule#NO_ALIGNMENT} will define the alignment for a path; if there are no rules\n   *     that return a value different from {@link AlignmentRule#NO_ALIGNMENT}, then {@link\n   *     AlignmentRule#NO_ALIGNMENT} is returned\n   * @return the composition rule\n   */\n  public static AlignmentRule compose(AlignmentRule... rules) {\n    return (String path) -> {\n      for (AlignmentRule r : rules) {\n        int align = r.alignment(path);\n        if (align != AlignmentRule.NO_ALIGNMENT) {\n          return align;\n        }\n      }\n\n      return AlignmentRule.NO_ALIGNMENT;\n    };\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectory.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.utils.CachedSupplier;\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableMap;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.primitives.Ints;\nimport com.google.common.util.concurrent.Futures;\nimport com.google.common.util.concurrent.ListenableFuture;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.ByteBuffer;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/** Representation of the central directory of a zip archive. */\nclass CentralDirectory {\n\n  /** Field in the central directory with the central directory signature. */\n  private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x02014b50, \"Signature\");\n\n  /** Field in the central directory with the \"made by\" code. */\n  private static final ZipField.F2 F_MADE_BY =\n      new ZipField.F2(F_SIGNATURE.endOffset(), \"Made by\", new ZipFieldInvariantNonNegative());\n\n  /** Field in the central directory with the minimum version required to extract the entry. */\n  @VisibleForTesting\n  static final ZipField.F2 F_VERSION_EXTRACT =\n      new ZipField.F2(\n          F_MADE_BY.endOffset(), \"Version to extract\", new ZipFieldInvariantNonNegative());\n\n  /** Field in the central directory with the GP bit flag. */\n  private static final ZipField.F2 F_GP_BIT =\n      new ZipField.F2(F_VERSION_EXTRACT.endOffset(), \"GP bit\");\n\n  /**\n   * Field in the central directory with the code of the compression method. See {@link\n   * CompressionMethod#fromCode(long)}.\n   */\n  private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), \"Method\");\n\n  /**\n   * Field in the central directory with the last modification time in MS-DOS format (see {@link\n   * MsDosDateTimeUtils#packTime(long)}).\n   */\n  private static final ZipField.F2 F_LAST_MOD_TIME =\n      new ZipField.F2(F_METHOD.endOffset(), \"Last modification time\");\n\n  /**\n   * Field in the central directory with the last modification date in MS-DOS format. See {@link\n   * MsDosDateTimeUtils#packDate(long)}.\n   */\n  private static final ZipField.F2 F_LAST_MOD_DATE =\n      new ZipField.F2(F_LAST_MOD_TIME.endOffset(), \"Last modification date\");\n\n  /**\n   * Field in the central directory with the CRC32 checksum of the entry. This will be zero for\n   * directories and files with no content.\n   */\n  private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), \"CRC32\");\n\n  /**\n   * Field in the central directory with the entry's compressed size, <em>i.e.</em>, the file on the\n   * archive. This will be the same as the uncompressed size if the method is {@link\n   * CompressionMethod#STORE}.\n   */\n  private static final ZipField.F4 F_COMPRESSED_SIZE =\n      new ZipField.F4(F_CRC32.endOffset(), \"Compressed size\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the central directory with the entry's uncompressed size, <em>i.e.</em>, the size the\n   * file will have when extracted from the zip. This will be zero for directories and empty files\n   * and will be the same as the compressed size if the method is {@link CompressionMethod#STORE}.\n   */\n  private static final ZipField.F4 F_UNCOMPRESSED_SIZE =\n      new ZipField.F4(\n          F_COMPRESSED_SIZE.endOffset(), \"Uncompressed size\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the central directory with the length of the file name. The file name is stored after\n   * the offset field ({@link #F_OFFSET}). The number of characters in the file name are stored in\n   * this field.\n   */\n  private static final ZipField.F2 F_FILE_NAME_LENGTH =\n      new ZipField.F2(\n          F_UNCOMPRESSED_SIZE.endOffset(), \"File name length\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the central directory with the length of the extra field. The extra field is stored\n   * after the file name ({@link #F_FILE_NAME_LENGTH}). The contents of this field are partially\n   * defined in the zip specification but we do not parse it.\n   */\n  private static final ZipField.F2 F_EXTRA_FIELD_LENGTH =\n      new ZipField.F2(\n          F_FILE_NAME_LENGTH.endOffset(), \"Extra field length\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the central directory with the length of the comment. The comment is stored after the\n   * extra field ({@link #F_EXTRA_FIELD_LENGTH}). We do not parse the comment.\n   */\n  private static final ZipField.F2 F_COMMENT_LENGTH =\n      new ZipField.F2(\n          F_EXTRA_FIELD_LENGTH.endOffset(), \"Comment length\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Number of the disk where the central directory starts. Because we do not support multi-file\n   * archives, this field has to have value {@code 0}.\n   */\n  private static final ZipField.F2 F_DISK_NUMBER_START =\n      new ZipField.F2(F_COMMENT_LENGTH.endOffset(), 0, \"Disk start\");\n\n  /** Internal attributes. This field can only contain one bit set, the {@link #ASCII_BIT}. */\n  private static final ZipField.F2 F_INTERNAL_ATTRIBUTES =\n      new ZipField.F2(F_DISK_NUMBER_START.endOffset(), \"Int attributes\");\n\n  /** External attributes. This field is ignored. */\n  private static final ZipField.F4 F_EXTERNAL_ATTRIBUTES =\n      new ZipField.F4(F_INTERNAL_ATTRIBUTES.endOffset(), \"Ext attributes\");\n\n  /**\n   * Offset into the archive where the entry starts. This is the offset to the local header (see\n   * {@link StoredEntry} for information on the local header), not to the file data itself. The file\n   * data, if there is any, will be stored after the local header.\n   */\n  private static final ZipField.F4 F_OFFSET =\n      new ZipField.F4(\n          F_EXTERNAL_ATTRIBUTES.endOffset(), \"Offset\", new ZipFieldInvariantNonNegative());\n\n  /** Maximum supported version to extract. */\n  private static final int MAX_VERSION_TO_EXTRACT = 20;\n\n  /**\n   * Bit that can be set on the internal attributes stating that the file is an ASCII file. We don't\n   * do anything with this information, but we check that nothing unexpected appears in the internal\n   * attributes.\n   */\n  private static final int ASCII_BIT = 1;\n\n  /** Contains all entries in the directory mapped from their names. */\n  private final Map<String, StoredEntry> entries;\n\n  /** The file where this directory belongs to. */\n  private final ZFile file;\n\n  /** Supplier that provides a byte representation of the central directory. */\n  private final CachedSupplier<byte[]> bytesSupplier;\n\n  /** Verify log for the central directory. */\n  private final VerifyLog verifyLog;\n\n  /**\n   * Creates a new, empty, central directory, for a given zip file.\n   *\n   * @param file the file\n   */\n  CentralDirectory(ZFile file) {\n    entries = Maps.newHashMap();\n    this.file = file;\n    bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n    verifyLog = file.getVerifyLog();\n  }\n\n  /**\n   * Reads the central directory data from a zip file, parses it, and creates the in-memory\n   * structure representing the directory.\n   *\n   * @param bytes the data of the central directory; the directory is read from the buffer's current\n   *     position; when this method terminates, the buffer's position is the first byte after the\n   *     directory\n   * @param count the number of entries expected in the central directory (usually read from the\n   *     {@link Eocd}).\n   * @param file the zip file this central directory belongs to\n   * @param storage the storage used to generate sources with entry data\n   * @return the central directory\n   * @throws IOException failed to read data from the zip, or the central directory is corrupted or\n   *     has unsupported features\n   */\n  static CentralDirectory makeFromData(ByteBuffer bytes, long count, ZFile file, ByteStorage storage)\n      throws IOException {\n    Preconditions.checkNotNull(bytes, \"bytes == null\");\n    Preconditions.checkArgument(count >= 0, \"count < 0\");\n\n    CentralDirectory directory = new CentralDirectory(file);\n\n    for (long i = 0; i < count; i++) {\n      try {\n        directory.readEntry(bytes, storage);\n      } catch (IOException e) {\n        throw new IOException(\n            \"Failed to read directory entry index \"\n                + i\n                + \" (total \"\n                + \"directory bytes read: \"\n                + bytes.position()\n                + \").\",\n            e);\n      }\n    }\n\n    return directory;\n  }\n\n  /**\n   * Creates a new central directory from the entries. This is used to build a new central directory\n   * from entries in the zip file.\n   *\n   * @param entries the entries in the zip file\n   * @param file the zip file itself\n   * @return the created central directory\n   */\n  static CentralDirectory makeFromEntries(Set<StoredEntry> entries, ZFile file) {\n    CentralDirectory directory = new CentralDirectory(file);\n    for (StoredEntry entry : entries) {\n      CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader();\n      Preconditions.checkArgument(\n          !directory.entries.containsKey(cdr.getName()), \"Duplicate filename\");\n      directory.entries.put(cdr.getName(), entry);\n    }\n\n    return directory;\n  }\n\n  /**\n   * Reads the next entry from the central directory and adds it to {@link #entries}.\n   *\n   * @param bytes the central directory's data, positioned starting at the beginning of the next\n   *     entry to read; when finished, the buffer's position will be at the first byte after the\n   *     entry\n   * @param storage the storage used to generate sources to store entry data\n   * @throws IOException failed to read the directory entry, either because of an I/O error, because\n   *     it is corrupt or contains unsupported features\n   */\n  private void readEntry(ByteBuffer bytes, ByteStorage storage) throws IOException {\n    F_SIGNATURE.verify(bytes);\n    long madeBy = F_MADE_BY.read(bytes);\n\n    long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes);\n    verifyLog.verify(\n        versionNeededToExtract <= MAX_VERSION_TO_EXTRACT,\n        \"Ignored unknown version needed to extract in zip directory entry: %s.\",\n        versionNeededToExtract);\n\n    long gpBit = F_GP_BIT.read(bytes);\n    GPFlags flags = GPFlags.from(gpBit);\n\n    long methodCode = F_METHOD.read(bytes);\n    CompressionMethod method = CompressionMethod.fromCode(methodCode);\n    verifyLog.verify(method != null, \"Unknown method in zip directory entry: %s.\", methodCode);\n\n    long lastModTime;\n    long lastModDate;\n    if (file.areTimestampsIgnored()) {\n      lastModTime = 0;\n      lastModDate = 0;\n      F_LAST_MOD_TIME.skip(bytes);\n      F_LAST_MOD_DATE.skip(bytes);\n    } else {\n      lastModTime = F_LAST_MOD_TIME.read(bytes);\n      lastModDate = F_LAST_MOD_DATE.read(bytes);\n    }\n\n    long crc32 = F_CRC32.read(bytes);\n    long compressedSize = F_COMPRESSED_SIZE.read(bytes);\n    long uncompressedSize = F_UNCOMPRESSED_SIZE.read(bytes);\n    int fileNameLength = Ints.checkedCast(F_FILE_NAME_LENGTH.read(bytes));\n    int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes));\n    int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes));\n\n    F_DISK_NUMBER_START.verify(bytes, verifyLog);\n    long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes);\n    verifyLog.verify(\n        (internalAttributes & ~ASCII_BIT) == 0,\n        \"Ignored invalid internal attributes: %s.\",\n        internalAttributes);\n\n    long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes);\n    long entryOffset = F_OFFSET.read(bytes);\n\n    long remainingSize = (long) fileNameLength + extraFieldLength + fileCommentLength;\n\n    if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) {\n      throw new IOException(\n          \"Directory entry should have \"\n              + remainingSize\n              + \" bytes remaining (name = \"\n              + fileNameLength\n              + \", extra = \"\n              + extraFieldLength\n              + \", comment = \"\n              + fileCommentLength\n              + \"), but it has \"\n              + bytes.remaining()\n              + \".\");\n    }\n\n    byte[] encodedFileName = new byte[fileNameLength];\n    bytes.get(encodedFileName);\n    String fileName = EncodeUtils.decode(encodedFileName, flags);\n\n    byte[] extraField = new byte[extraFieldLength];\n    bytes.get(extraField);\n\n    byte[] fileCommentField = new byte[fileCommentLength];\n    bytes.get(fileCommentField);\n\n    /*\n     * Tricky: to create a CentralDirectoryHeader we need the future that will hold the result\n     * of the compress information. But, to actually create the result of the compress\n     * information we need the CentralDirectoryHeader\n     */\n    ListenableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =\n        Futures.immediateFuture(\n            new CentralDirectoryHeaderCompressInfo(method, compressedSize, versionNeededToExtract));\n    CentralDirectoryHeader centralDirectoryHeader =\n        new CentralDirectoryHeader(\n            fileName,\n            encodedFileName,\n            uncompressedSize,\n            compressInfo,\n            flags,\n            file,\n            lastModTime,\n            lastModDate);\n    centralDirectoryHeader.setMadeBy(madeBy);\n    centralDirectoryHeader.setLastModTime(lastModTime);\n    centralDirectoryHeader.setLastModDate(lastModDate);\n    centralDirectoryHeader.setCrc32(crc32);\n    centralDirectoryHeader.setInternalAttributes(internalAttributes);\n    centralDirectoryHeader.setExternalAttributes(externalAttributes);\n    centralDirectoryHeader.setOffset(entryOffset);\n    centralDirectoryHeader.setExtraFieldNoNotify(new ExtraField(extraField));\n    centralDirectoryHeader.setComment(fileCommentField);\n\n    StoredEntry entry;\n\n    try {\n      entry = new StoredEntry(centralDirectoryHeader, file, null, storage);\n    } catch (IOException e) {\n      throw new IOException(\"Failed to read stored entry '\" + fileName + \"'.\", e);\n    }\n\n    if (entries.containsKey(fileName)) {\n      verifyLog.log(\"File file contains duplicate file '\" + fileName + \"'.\");\n    }\n\n    entries.put(fileName, entry);\n  }\n\n  /**\n   * Obtains all the entries in the central directory.\n   *\n   * @return all entries on a non-modifiable map\n   */\n  Map<String, StoredEntry> getEntries() {\n    return ImmutableMap.copyOf(entries);\n  }\n\n  /**\n   * Obtains whether the Central Directory contains any files with Zip64 file extensions.\n   *\n   * <p>At the present time, files in the Zip64 format are not supported, so this method returns\n   * false.\n   *\n   * @return false, as Zip64 formatted files are not supported\n   */\n  boolean containsZip64Files() {\n    return false;\n  }\n\n  /**\n   * Obtains the byte representation of the central directory.\n   *\n   * @return a byte array containing the whole central directory\n   * @throws IOException failed to write the byte array\n   */\n  byte[] toBytes() throws IOException {\n    return bytesSupplier.get();\n  }\n\n  /**\n   * Computes the byte representation of the central directory.\n   *\n   * @return a byte array containing the whole central directory\n   * @throws UncheckedIOException failed to write the byte array\n   */\n  private byte[] computeByteRepresentation() {\n\n    List<StoredEntry> sorted = Lists.newArrayList(entries.values());\n    Collections.sort(sorted, StoredEntry.COMPARE_BY_NAME);\n\n    CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()];\n    CentralDirectoryHeaderCompressInfo[] compressInfos =\n        new CentralDirectoryHeaderCompressInfo[entries.size()];\n    byte[][] encodedFileNames = new byte[entries.size()][];\n    byte[][] extraFields = new byte[entries.size()][];\n    byte[][] comments = new byte[entries.size()][];\n\n    try {\n      /*\n       * First collect all the data and compute the total size of the central directory.\n       */\n      int idx = 0;\n      int total = 0;\n      for (StoredEntry entry : sorted) {\n        cdhs[idx] = entry.getCentralDirectoryHeader();\n        compressInfos[idx] = cdhs[idx].getCompressionInfoWithWait();\n        encodedFileNames[idx] = cdhs[idx].getEncodedFileName();\n        extraFields[idx] = new byte[cdhs[idx].getExtraField().size()];\n        cdhs[idx].getExtraField().write(ByteBuffer.wrap(extraFields[idx]));\n        comments[idx] = cdhs[idx].getComment();\n\n        total +=\n            F_OFFSET.endOffset()\n                + encodedFileNames[idx].length\n                + extraFields[idx].length\n                + comments[idx].length;\n        idx++;\n      }\n\n      ByteBuffer out = ByteBuffer.allocate(total);\n\n      for (idx = 0; idx < entries.size(); idx++) {\n        F_SIGNATURE.write(out);\n        F_MADE_BY.write(out, cdhs[idx].getMadeBy());\n        F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract());\n        F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue());\n        F_METHOD.write(out, compressInfos[idx].getMethod().methodCode);\n\n        if (file.areTimestampsIgnored()) {\n          F_LAST_MOD_TIME.write(out, 0);\n          F_LAST_MOD_DATE.write(out, 0);\n        } else {\n          F_LAST_MOD_TIME.write(out, cdhs[idx].getLastModTime());\n          F_LAST_MOD_DATE.write(out, cdhs[idx].getLastModDate());\n        }\n\n        F_CRC32.write(out, cdhs[idx].getCrc32());\n        F_COMPRESSED_SIZE.write(out, compressInfos[idx].getCompressedSize());\n        F_UNCOMPRESSED_SIZE.write(out, cdhs[idx].getUncompressedSize());\n\n        F_FILE_NAME_LENGTH.write(out, cdhs[idx].getEncodedFileName().length);\n        F_EXTRA_FIELD_LENGTH.write(out, cdhs[idx].getExtraField().size());\n        F_COMMENT_LENGTH.write(out, cdhs[idx].getComment().length);\n        F_DISK_NUMBER_START.write(out);\n        F_INTERNAL_ATTRIBUTES.write(out, cdhs[idx].getInternalAttributes());\n        F_EXTERNAL_ATTRIBUTES.write(out, cdhs[idx].getExternalAttributes());\n        F_OFFSET.write(out, cdhs[idx].getOffset());\n\n        out.put(encodedFileNames[idx]);\n        out.put(extraFields[idx]);\n        out.put(comments[idx]);\n      }\n\n      return out.array();\n    } catch (IOException e) {\n      throw new IOExceptionWrapper(e);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeader.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils;\nimport com.google.common.base.Verify;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Future;\n\n/**\n * The Central Directory Header contains information about files stored in the zip. Instances of\n * this class contain information for files that already are in the zip and, for which the data was\n * read from the Central Directory. But some instances of this class are used for new files. Because\n * instances of this class can refer to files not yet on the zip, some of the fields may not be\n * filled in, or may be filled in with default values.\n *\n * <p>Because compression decision is done lazily, some data is stored with futures.\n */\npublic class CentralDirectoryHeader implements Cloneable {\n\n  /**\n   * Default \"version made by\" field: upper byte needs to be 0 to set to MS-DOS compatibility. Lower\n   * byte can be anything, really. We use 18 because aapt uses 17 :)\n   */\n  private static final int DEFAULT_VERSION_MADE_BY = 0x0018;\n\n  private static final byte[] EMPTY_COMMENT = new byte[0];\n\n  /** Name of the file. */\n  private final String name;\n\n  /** CRC32 of the data. 0 if not yet computed. */\n  private long crc32;\n\n  /** Size of the file uncompressed. 0 if the file has no data. */\n  private long uncompressedSize;\n\n  /** Code of the program that made the zip. We actually don't care about this. */\n  private long madeBy;\n\n  /** General-purpose bit flag. */\n  private GPFlags gpBit;\n\n  /** Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}). */\n  private long lastModTime;\n\n  /** Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}). */\n  private long lastModDate;\n\n  /**\n   * Extra data field contents. This field follows a specific structure according to the\n   * specification.\n   */\n  private ExtraField extraField;\n\n  /** File comment. */\n  private byte[] comment;\n\n  /** File internal attributes. */\n  private long internalAttributes;\n\n  /** File external attributes. */\n  private long externalAttributes;\n\n  /**\n   * Offset in the file where the data is located. This will be -1 if the header corresponds to a\n   * new file that is not yet written in the zip and, therefore, has no written data.\n   */\n  private long offset;\n\n  /** Encoded file name. */\n  private byte[] encodedFileName;\n\n  /** Compress information that may not have been computed yet due to lazy compression. */\n  private final Future<CentralDirectoryHeaderCompressInfo> compressInfo;\n\n  /** The file this header belongs to. */\n  private final ZFile file;\n\n  /**\n   * Creates data for a file.\n   *\n   * @param name the file name\n   * @param encodedFileName the encoded file name, this array will be owned by the header\n   * @param uncompressedSize the uncompressed file size\n   * @param compressInfo computation that defines the compression information\n   * @param flags flags used in the entry\n   * @param zFile the file this header belongs to\n   */\n  CentralDirectoryHeader(\n      String name,\n      byte[] encodedFileName,\n      long uncompressedSize,\n      Future<CentralDirectoryHeaderCompressInfo> compressInfo,\n      GPFlags flags,\n      ZFile zFile) {\n    this(\n        name,\n        encodedFileName,\n        uncompressedSize,\n        compressInfo,\n        flags,\n        zFile,\n        MsDosDateTimeUtils.packCurrentTime(),\n        MsDosDateTimeUtils.packCurrentDate());\n  }\n\n  CentralDirectoryHeader(\n      String name,\n      byte[] encodedFileName,\n      long uncompressedSize,\n      Future<CentralDirectoryHeaderCompressInfo> compressInfo,\n      GPFlags flags,\n      ZFile zFile,\n      long currentTime,\n      long currentDate) {\n    this.name = name;\n    this.uncompressedSize = uncompressedSize;\n    crc32 = 0;\n\n    /*\n     * Set sensible defaults for the rest.\n     */\n    madeBy = DEFAULT_VERSION_MADE_BY;\n\n    gpBit = flags;\n    lastModTime = currentTime;\n    lastModDate = currentDate;\n    extraField = ExtraField.EMPTY;\n    comment = EMPTY_COMMENT;\n    internalAttributes = 0;\n    externalAttributes = 0;\n    offset = -1;\n    this.encodedFileName = encodedFileName;\n    this.compressInfo = compressInfo;\n    file = zFile;\n  }\n\n  public CentralDirectoryHeader link(String name, byte[] encodedFileName, GPFlags flags, ZFile file) {\n    var newData = new CentralDirectoryHeader(name,\n      encodedFileName,\n      uncompressedSize,\n      compressInfo,\n      flags,\n      file,\n      lastModTime,\n      lastModDate);\n    newData.extraField = extraField;\n    newData.offset = -1;\n    newData.internalAttributes = internalAttributes;\n    newData.externalAttributes = externalAttributes;\n    newData.comment = comment;\n    newData.madeBy = madeBy;\n    newData.crc32 = crc32;\n    return newData;\n  }\n\n  /**\n   * Obtains the name of the file.\n   *\n   * @return the name\n   */\n  public String getName() {\n    return name;\n  }\n\n  /**\n   * Obtains the size of the uncompressed file.\n   *\n   * @return the size of the file\n   */\n  public long getUncompressedSize() {\n    return uncompressedSize;\n  }\n\n  /**\n   * Obtains the CRC32 of the data.\n   *\n   * @return the CRC32, 0 if not yet computed\n   */\n  public long getCrc32() {\n    return crc32;\n  }\n\n  /**\n   * Sets the CRC32 of the data.\n   *\n   * @param crc32 the CRC 32\n   */\n  void setCrc32(long crc32) {\n    this.crc32 = crc32;\n  }\n\n  /**\n   * Obtains the code of the program that made the zip.\n   *\n   * @return the code\n   */\n  public long getMadeBy() {\n    return madeBy;\n  }\n\n  /**\n   * Sets the code of the progtram that made the zip.\n   *\n   * @param madeBy the code\n   */\n  void setMadeBy(long madeBy) {\n    this.madeBy = madeBy;\n  }\n\n  /**\n   * Obtains the general-purpose bit flag.\n   *\n   * @return the bit flag\n   */\n  public GPFlags getGpBit() {\n    return gpBit;\n  }\n\n  /**\n   * Obtains the last modification time of the entry.\n   *\n   * @return the last modification time in MS-DOS format (see {@link\n   *     MsDosDateTimeUtils#packTime(long)})\n   */\n  public long getLastModTime() {\n    return lastModTime;\n  }\n\n  /**\n   * Sets the last modification time of the entry.\n   *\n   * @param lastModTime the last modification time in MS-DOS format (see {@link\n   *     MsDosDateTimeUtils#packTime(long)})\n   */\n  void setLastModTime(long lastModTime) {\n    this.lastModTime = lastModTime;\n  }\n\n  /**\n   * Obtains the last modification date of the entry.\n   *\n   * @return the last modification date in MS-DOS format (see {@link\n   *     MsDosDateTimeUtils#packDate(long)})\n   */\n  public long getLastModDate() {\n    return lastModDate;\n  }\n\n  /**\n   * Sets the last modification date of the entry.\n   *\n   * @param lastModDate the last modification date in MS-DOS format (see {@link\n   *     MsDosDateTimeUtils#packDate(long)})\n   */\n  void setLastModDate(long lastModDate) {\n    this.lastModDate = lastModDate;\n  }\n\n  /**\n   * Obtains the data in the extra field.\n   *\n   * @return the data (returns an empty array if there is none)\n   */\n  public ExtraField getExtraField() {\n    return extraField;\n  }\n\n  /**\n   * Sets the data in the extra field.\n   *\n   * @param extraField the data to set\n   */\n  public void setExtraField(ExtraField extraField) {\n    setExtraFieldNoNotify(extraField);\n    file.centralDirectoryChanged();\n  }\n\n  /**\n   * Sets the data in the extra field, but does not notify {@link ZFile}. This method is invoked\n   * when the {@link ZFile} knows the extra field is being set.\n   *\n   * @param extraField the data to set\n   */\n  void setExtraFieldNoNotify(ExtraField extraField) {\n    this.extraField = extraField;\n  }\n\n  /**\n   * Obtains the entry's comment.\n   *\n   * @return the comment (returns an empty array if there is no comment)\n   */\n  public byte[] getComment() {\n    return comment;\n  }\n\n  /**\n   * Sets the entry's comment.\n   *\n   * @param comment the comment\n   */\n  void setComment(byte[] comment) {\n    this.comment = comment;\n  }\n\n  /**\n   * Obtains the entry's internal attributes.\n   *\n   * @return the entry's internal attributes\n   */\n  public long getInternalAttributes() {\n    return internalAttributes;\n  }\n\n  /**\n   * Sets the entry's internal attributes.\n   *\n   * @param internalAttributes the entry's internal attributes\n   */\n  void setInternalAttributes(long internalAttributes) {\n    this.internalAttributes = internalAttributes;\n  }\n\n  /**\n   * Obtains the entry's external attributes.\n   *\n   * @return the entry's external attributes\n   */\n  public long getExternalAttributes() {\n    return externalAttributes;\n  }\n\n  /**\n   * Sets the entry's external attributes.\n   *\n   * @param externalAttributes the entry's external attributes\n   */\n  void setExternalAttributes(long externalAttributes) {\n    this.externalAttributes = externalAttributes;\n  }\n\n  /**\n   * Obtains the offset in the zip file where this entry's data is.\n   *\n   * @return the offset or {@code -1} if the file has no data in the zip and, therefore, data is\n   *     stored in memory\n   */\n  public long getOffset() {\n    return offset;\n  }\n\n  /**\n   * Sets the offset in the zip file where this entry's data is.\n   *\n   * @param offset the offset or {@code -1} if the file is new and has no data in the zip yet\n   */\n  void setOffset(long offset) {\n    this.offset = offset;\n  }\n\n  /**\n   * Obtains the encoded file name.\n   *\n   * @return the encoded file name\n   */\n  public byte[] getEncodedFileName() {\n    return encodedFileName;\n  }\n\n  /** Resets the deferred CRC flag in the GP flags. */\n  void resetDeferredCrc() {\n    /*\n     * We actually create a new set of flags. Since the only information we care about is the\n     * UTF-8 encoding, we'll just create a brand new object.\n     */\n    gpBit = GPFlags.make(gpBit.isUtf8FileName());\n  }\n\n  @Override\n  protected CentralDirectoryHeader clone() throws CloneNotSupportedException {\n    CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone();\n    cdr.extraField = extraField;\n    cdr.comment = Arrays.copyOf(comment, comment.length);\n    cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length);\n    return cdr;\n  }\n\n  /**\n   * Obtains the future with the compression information.\n   *\n   * @return the information\n   */\n  public Future<CentralDirectoryHeaderCompressInfo> getCompressionInfo() {\n    return compressInfo;\n  }\n\n  /**\n   * Equivalent to {@code getCompressionInfo().get()} but masking the possible exceptions and\n   * guaranteeing non-{@code null} return.\n   *\n   * @return the result of the future\n   * @throws IOException failed to get the information\n   */\n  public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait() throws IOException {\n    try {\n      CentralDirectoryHeaderCompressInfo info = getCompressionInfo().get();\n      Verify.verifyNotNull(info, \"info == null\");\n      return info;\n    } catch (InterruptedException e) {\n      throw new IOException(\"Interrupted while waiting for compression information.\", e);\n    } catch (ExecutionException e) {\n      throw new IOException(\"Execution of compression failed.\", e);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n\n/**\n * Information stored in the {@link CentralDirectoryHeader} that is related to compression and may\n * need to be computed lazily.\n */\npublic class CentralDirectoryHeaderCompressInfo {\n\n  /** Version of zip file that only supports stored files. */\n  public static final long VERSION_WITH_STORE_FILES_ONLY = 10L;\n\n  /** Version of zip file that only supports directories and deflated files. */\n  public static final long VERSION_WITH_DIRECTORIES_AND_DEFLATE = 20L;\n\n  /** Version of zip file that only supports ZIP64 format extensions */\n  public static final long VERSION_WITH_ZIP64_EXTENSIONS = 45L;\n\n  /** Version of zip file that uses central file encryption and version 2 of the Zip64 EOCD */\n  public static final long VERSION_WITH_CENTRAL_FILE_ENCRYPTION = 62L;\n\n  /** The compression method. */\n  private final CompressionMethod method;\n\n  /** Size of the file compressed. 0 if the file has no data. */\n  private final long compressedSize;\n\n  /** Version needed to extract the zip. */\n  private final long versionExtract;\n\n  /**\n   * Creates new compression information for the central directory header.\n   *\n   * @param method the compression method\n   * @param compressedSize the compressed size\n   * @param versionToExtract minimum version to extract (typically {@link\n   *     #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE})\n   */\n  public CentralDirectoryHeaderCompressInfo(\n      CompressionMethod method, long compressedSize, long versionToExtract) {\n    this.method = method;\n    this.compressedSize = compressedSize;\n    versionExtract = versionToExtract;\n  }\n\n  /**\n   * Creates new compression information for the central directory header.\n   *\n   * @param header the header this information relates to\n   * @param method the compression method\n   * @param compressedSize the compressed size\n   */\n  public CentralDirectoryHeaderCompressInfo(\n      CentralDirectoryHeader header, CompressionMethod method, long compressedSize) {\n    this.method = method;\n    this.compressedSize = compressedSize;\n\n   if (header.getName().endsWith(\"/\") || method == CompressionMethod.DEFLATE) {\n      /*\n       * Directories and compressed files only in version 2.0.\n       */\n      versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE;\n    } else {\n      versionExtract = VERSION_WITH_STORE_FILES_ONLY;\n    }\n  }\n\n  /**\n   * Obtains the compression data size.\n   *\n   * @return the compressed data size\n   */\n  public long getCompressedSize() {\n    return compressedSize;\n  }\n\n  /**\n   * Obtains the compression method.\n   *\n   * @return the compression method\n   */\n  public CompressionMethod getMethod() {\n    return method;\n  }\n\n  /**\n   * Obtains the minimum version for extract.\n   *\n   * @return the minimum version\n   */\n  long getVersionExtract() {\n    return versionExtract;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CompressionMethod.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport javax.annotation.Nullable;\n\n/** Enumeration with all known compression methods. */\npublic enum CompressionMethod {\n  /** STORE method: data is stored without any compression. */\n  STORE(0),\n\n  /** DEFLATE method: data is stored compressed using the DEFLATE algorithm. */\n  DEFLATE(8);\n\n  /** Code, within the zip file, that identifies this compression method. */\n  int methodCode;\n\n  /**\n   * Creates a new compression method.\n   *\n   * @param methodCode the code used in the zip file that identifies the compression method\n   */\n  CompressionMethod(int methodCode) {\n    this.methodCode = methodCode;\n  }\n\n  /**\n   * Obtains the compression method that corresponds to the provided code.\n   *\n   * @param code the code\n   * @return the method or {@code null} if no method has the provided code\n   */\n  @Nullable\n  static CompressionMethod fromCode(long code) {\n    for (CompressionMethod method : values()) {\n      if (method.methodCode == code) {\n        return method;\n      }\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/CompressionResult.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\n\n/** Result of compressing data. */\npublic class CompressionResult {\n\n  /** The compression method used. */\n  private final CompressionMethod compressionMethod;\n\n  /** The resulting data. */\n  private final CloseableByteSource source;\n\n  /**\n   * Size of the compressed source. Kept because {@code source.size()} can throw {@code\n   * IOException}.\n   */\n  private final long mSize;\n\n  /**\n   * Creates a new compression result.\n   *\n   * @param source the data source\n   * @param method the compression method\n   */\n  public CompressionResult(CloseableByteSource source, CompressionMethod method, long size) {\n    compressionMethod = method;\n    this.source = source;\n    mSize = size;\n  }\n\n  /**\n   * Obtains the compression method.\n   *\n   * @return the compression method\n   */\n  public CompressionMethod getCompressionMethod() {\n    return compressionMethod;\n  }\n\n  /**\n   * Obtains the compressed data.\n   *\n   * @return the data, the resulting array should not be modified\n   */\n  public CloseableByteSource getSource() {\n    return source;\n  }\n\n  /**\n   * Obtains the size of the compression result.\n   *\n   * @return the size\n   */\n  public long getSize() {\n    return mSize;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Compressor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.util.concurrent.ListenableFuture;\n\n/**\n * A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}.\n * Compressors are asynchronous: compressing results in a {@code ListenableFuture} that will contain\n * the compression result.\n */\npublic interface Compressor {\n\n  /**\n   * Compresses an entry source.\n   *\n   * @param source the source to compress\n   * @param storage a byte storage from where the compressor can obtain byte sources to work\n   * @return a future that will eventually contain the compression result\n   */\n  ListenableFuture<CompressionResult> compress(CloseableByteSource source, ByteStorage storage);\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/DataDescriptorType.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/**\n * Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data\n * is not known when the data is being written and cannot be placed in the file's local header. In\n * those cases, after the file data itself, a data descriptor is placed after the entry's contents.\n *\n * <p>While the zip specification says the data descriptor should be used but it is optional. We\n * record also whether the data descriptor contained the 4-byte signature at the start of the block\n * or not.\n */\npublic enum DataDescriptorType {\n  /** The entry has no data descriptor. */\n  NO_DATA_DESCRIPTOR(0),\n\n  /** The entry has a data descriptor that does not contain a signature. */\n  DATA_DESCRIPTOR_WITHOUT_SIGNATURE(12),\n\n  /** The entry has a data descriptor that contains a signature. */\n  DATA_DESCRIPTOR_WITH_SIGNATURE(16);\n\n  /** The number of bytes the data descriptor spans. */\n  public int size;\n\n  /**\n   * Creates a new data descriptor.\n   *\n   * @param size the number of bytes the data descriptor spans\n   */\n  DataDescriptorType(int size) {\n    this.size = size;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/EncodeUtils.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport static java.nio.charset.StandardCharsets.US_ASCII;\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.charset.CharacterCodingException;\nimport java.nio.charset.Charset;\nimport java.nio.charset.CodingErrorAction;\n\n/** Utilities to encode and decode file names in zips. */\npublic class EncodeUtils {\n\n  /** Utility class: no constructor. */\n  private EncodeUtils() {\n    /*\n     * Nothing to do.\n     */\n  }\n\n  /**\n   * Decodes a file name.\n   *\n   * @param bytes the raw data buffer to read from\n   * @param length the number of bytes in the raw data buffer containing the string to decode\n   * @param flags the zip entry flags\n   * @return the decode file name\n   */\n  public static String decode(ByteBuffer bytes, int length, GPFlags flags) throws IOException {\n    if (bytes.remaining() < length) {\n      throw new IOException(\n          \"Only \"\n              + bytes.remaining()\n              + \" bytes exist in the buffer, but \"\n              + \"length is \"\n              + length\n              + \".\");\n    }\n\n    byte[] stringBytes = new byte[length];\n    bytes.get(stringBytes);\n    return decode(stringBytes, flags);\n  }\n\n  /**\n   * Decodes a file name.\n   *\n   * @param data the raw data\n   * @param flags the zip entry flags\n   * @return the decode file name\n   */\n  public static String decode(byte[] data, GPFlags flags) {\n    return decode(data, flagsCharset(flags));\n  }\n\n  /**\n   * Decodes a file name.\n   *\n   * @param data the raw data\n   * @param charset the charset to use\n   * @return the decode file name\n   */\n  private static String decode(byte[] data, Charset charset) {\n    try {\n      return charset\n          .newDecoder()\n          .onMalformedInput(CodingErrorAction.REPORT)\n          .decode(ByteBuffer.wrap(data))\n          .toString();\n    } catch (CharacterCodingException e) {\n      // If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default\n      // behavior (usually replacing invalid characters).\n      if (charset.equals(US_ASCII)) {\n        return decode(data, UTF_8);\n      } else {\n        return charset.decode(ByteBuffer.wrap(data)).toString();\n      }\n    }\n  }\n\n  /**\n   * Encodes a file name.\n   *\n   * @param name the name to encode\n   * @param flags the zip entry flags\n   * @return the encoded file name\n   */\n  public static byte[] encode(String name, GPFlags flags) {\n    Charset charset = flagsCharset(flags);\n    ByteBuffer bytes = charset.encode(name);\n    byte[] result = new byte[bytes.remaining()];\n    bytes.get(result);\n    return result;\n  }\n\n  /**\n   * Obtains the charset to encode and decode zip entries, given a set of flags.\n   *\n   * @param flags the flags\n   * @return the charset to use\n   */\n  private static Charset flagsCharset(GPFlags flags) {\n    if (flags.isUtf8FileName()) {\n      return UTF_8;\n    } else {\n      return US_ASCII;\n    }\n  }\n\n  /**\n   * Checks if some text may be encoded using ASCII.\n   *\n   * @param text the text to check\n   * @return can it be encoded using ASCII?\n   */\n  public static boolean canAsciiEncode(String text) {\n    return US_ASCII.newEncoder().canEncode(text);\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.utils.CachedSupplier;\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport com.google.common.primitives.Ints;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.ByteBuffer;\n\n/** End Of Central Directory record in a zip file. */\nclass Eocd {\n\n  /** Max total records that can be specified by the standard EOCD. */\n  static final long MAX_TOTAL_RECORDS = 0xFFFFL;\n\n  /** Max size of the Central Directory that can be specified by the standard EOCD. */\n  static final long MAX_CD_SIZE = 0xFFFFFFFFL;\n\n  /** Max offset of the Central Directory that can be specified by the standard EOCD. */\n  static final long MAX_CD_OFFSET = 0xFFFFFFFFL;\n\n  /** Field in the record: the record signature, fixed at this value by the specification. */\n  private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, \"EOCD signature\");\n\n  /**\n   * Field in the record: the number of the disk where the EOCD is located. It has to be zero\n   * because we do not support multi-file archives.\n   */\n  private static final ZipField.F2 F_NUMBER_OF_DISK =\n      new ZipField.F2(F_SIGNATURE.endOffset(), 0, \"Number of this disk\");\n\n  /**\n   * Field in the record: the number of the disk where the Central Directory starts. Has to be zero\n   * because we do not support multi-file archives.\n   */\n  private static final ZipField.F2 F_DISK_CD_START =\n      new ZipField.F2(F_NUMBER_OF_DISK.endOffset(), 0, \"Disk where CD starts\");\n\n  /**\n   * Field in the record: the number of entries in the Central Directory on this disk. Because we do\n   * not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}.\n   */\n  private static final ZipField.F2 F_RECORDS_DISK =\n      new ZipField.F2(\n          F_DISK_CD_START.endOffset(), \"Record on disk count\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the record: the total number of entries in the Central Directory. This value will be\n   * {@link #MAX_TOTAL_RECORDS} if the file is in the Zip64 format, and the Central Directory holds\n   * at least {@link #MAX_TOTAL_RECORDS} entries.\n   */\n  private static final ZipField.F2 F_RECORDS_TOTAL =\n      new ZipField.F2(\n          F_RECORDS_DISK.endOffset(),\n          \"Total records\",\n          new ZipFieldInvariantNonNegative(),\n          new ZipFieldInvariantMaxValue(Integer.MAX_VALUE));\n\n  /**\n   * Field in the record: number of bytes of the Central Directory. This is not private because it\n   * is required in unit tests. This value will be {@link #MAX_CD_SIZE} if the file is in the Zip64\n   * format, and the Central Directory is at least {@link #MAX_CD_SIZE} bytes.\n   */\n  @VisibleForTesting\n  static final ZipField.F4 F_CD_SIZE =\n      new ZipField.F4(\n          F_RECORDS_TOTAL.endOffset(), \"Directory size\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the record: offset, from the archive start, where the Central Directory starts. This\n   * is not private because it is required in unit tests. This value will be {@link #MAX_CD_OFFSET}\n   * if the file is in the Zip64 format, and the Central Directory is at least\n   * {@link #MAX_CD_OFFSET} bytes.\n   */\n  @VisibleForTesting\n  static final ZipField.F4 F_CD_OFFSET =\n      new ZipField.F4(\n          F_CD_SIZE.endOffset(), \"Directory offset\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the record: number of bytes of the file comment (located at the end of the EOCD\n   * record).\n   */\n  private static final ZipField.F2 F_COMMENT_SIZE =\n      new ZipField.F2(\n          F_CD_OFFSET.endOffset(), \"File comment size\", new ZipFieldInvariantNonNegative());\n\n  /** Number of entries in the central directory. */\n  private final long totalRecords;\n\n  /** Offset from the beginning of the archive where the Central Directory is located. */\n  private final long directoryOffset;\n\n  /** Number of bytes of the Central Directory. */\n  private final long directorySize;\n\n  /** Contents of the EOCD comment. */\n  private final byte[] comment;\n\n  /** Supplier of the byte representation of the EOCD. */\n  private final CachedSupplier<byte[]> byteSupplier;\n\n  /**\n   * Creates a new EOCD, reading it from a byte source. This method will parse the byte source and\n   * obtain the EOCD. It will check that the byte source starts with the EOCD signature.\n   *\n   * @param bytes the byte buffer with the EOCD data; when this method finishes, the byte buffer's\n   *     position will have moved to the end of the EOCD\n   * @throws IOException failed to read information or the EOCD data is corrupt or invalid\n   */\n  Eocd(ByteBuffer bytes) throws IOException {\n\n    /*\n     * Read the EOCD record.\n     */\n    F_SIGNATURE.verify(bytes);\n    F_NUMBER_OF_DISK.verify(bytes);\n    F_DISK_CD_START.verify(bytes);\n    long totalRecords1 = F_RECORDS_DISK.read(bytes);\n    long totalRecords2 = F_RECORDS_TOTAL.read(bytes);\n    long directorySize = F_CD_SIZE.read(bytes);\n    long directoryOffset = F_CD_OFFSET.read(bytes);\n    int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes));\n\n    /*\n     * Some sanity checks.\n     */\n    if (totalRecords1 != totalRecords2) {\n      throw new IOException(\n          \"Zip states records split in multiple disks, which is not \" + \"supported.\");\n    }\n\n    Verify.verify(totalRecords1 <= Integer.MAX_VALUE);\n\n    totalRecords = Ints.checkedCast(totalRecords1);\n    this.directorySize = directorySize;\n    this.directoryOffset = directoryOffset;\n\n    if (bytes.remaining() < commentSize) {\n      throw new IOException(\n          \"Corrupt EOCD record: not enough data for comment (comment \"\n              + \"size is \"\n              + commentSize\n              + \").\");\n    }\n\n    comment = new byte[commentSize];\n    bytes.get(comment);\n    byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n  }\n\n  /**\n   * Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has just\n   * been generated. The EOCD will be generated without any comment.\n   *\n   * @param totalRecords total number of records in the directory\n   * @param directoryOffset offset, since beginning of archive, where the Central Directory is\n   *     located\n   * @param directorySize number of bytes of the Central Directory\n   * @param comment the EOCD comment\n   */\n  Eocd(long totalRecords, long directoryOffset, long directorySize, byte[] comment) {\n    Preconditions.checkArgument(totalRecords >= 0, \"totalRecords < 0\");\n    Preconditions.checkArgument(directoryOffset >= 0, \"directoryOffset < 0\");\n    Preconditions.checkArgument(directorySize >= 0, \"directorySize < 0\");\n\n    this.totalRecords = totalRecords;\n    this.directoryOffset = directoryOffset;\n    this.directorySize = directorySize;\n    this.comment = comment;\n    byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n  }\n\n  /**\n   * Obtains the number of records in the Central Directory.\n   *\n   * @return the number of records\n   */\n  long getTotalRecords() {\n    return totalRecords;\n  }\n\n  /**\n   * Obtains the offset since the beginning of the zip archive where the Central Directory is\n   * located.\n   *\n   * @return the offset where the Central Directory is located\n   */\n  long getDirectoryOffset() {\n    return directoryOffset;\n  }\n\n  /**\n   * Obtains the size of the Central Directory.\n   *\n   * @return the number of bytes that make up the Central Directory\n   */\n  long getDirectorySize() {\n    return directorySize;\n  }\n\n  /**\n   * Obtains the size of the EOCD.\n   *\n   * @return the size, in bytes, of the EOCD\n   */\n  long getEocdSize() {\n    return (long) F_COMMENT_SIZE.endOffset() + comment.length;\n  }\n\n  /**\n   * Generates the EOCD data.\n   *\n   * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes\n   * @throws IOException failed to generate the EOCD data\n   */\n  byte[] toBytes() throws IOException {\n    return byteSupplier.get();\n  }\n\n  /**\n   * Obtains the comment in the EOCD.\n   *\n   * @return the comment exactly as it is represented in the file (no encoding conversion is\n   * done)\n   */\n  byte[] getComment() {\n    byte[] commentCopy = new byte[comment.length];\n    System.arraycopy(comment, 0, commentCopy, 0, comment.length);\n    return commentCopy;\n  }\n\n  /**\n   * Computes the byte representation of the EOCD.\n   *\n   * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes\n   * @throws UncheckedIOException failed to generate the EOCD data\n   */\n  private byte[] computeByteRepresentation() {\n    ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length);\n\n    try {\n      F_SIGNATURE.write(out);\n      F_NUMBER_OF_DISK.write(out);\n      F_DISK_CD_START.write(out);\n      F_RECORDS_DISK.write(out, totalRecords);\n      F_RECORDS_TOTAL.write(out, totalRecords);\n      F_CD_SIZE.write(out, directorySize);\n      F_CD_OFFSET.write(out, directoryOffset);\n      F_COMMENT_SIZE.write(out, comment.length);\n      out.put(comment);\n\n      return out.array();\n    } catch (IOException e) {\n      throw new IOExceptionWrapper(e);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/EocdGroup.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport com.google.common.primitives.Ints;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport javax.annotation.Nullable;\n\n/**\n * The collection of all data stored in all End of Central Directory records in the zip file. The\n * {@code EOCDGroup} is meant to collect and manage all the information about the {@link Eocd},\n * {@link Zip64EocdLocator}, and the {@link Zip64Eocd} in one place.\n */\npublic class EocdGroup {\n\n  /** Minimum size the EOCD can have. */\n  private static final int MIN_EOCD_SIZE = 22;\n\n  /** Maximum size for the EOCD. */\n  private static final int MAX_EOCD_COMMENT_SIZE = 65535;\n\n  /** How many bytes to look back from the end of the file to look for the EOCD signature. */\n  private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE;\n\n  /** Signature of the Zip64 EOCD locator record. */\n  private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;\n\n  /** Signature of the EOCD record. */\n  private static final long EOCD_SIGNATURE = 0x06054b50;\n\n  /**\n   * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the one\n   * that exists on disk is no longer valid (because the zip has been changed).\n   *\n   * <p>If the EOCD is deleted because the zip has been changed and the old EOCD was no longer\n   * valid, then {@link #eocdComment} will contain the comment saved from the EOCD.\n   */\n  @Nullable\n  private FileUseMapEntry<Eocd> eocdEntry;\n\n  /**\n   * The EOCD locator entry. Will be {@code null} if there is no EOCD (because the zip is new),\n   * the EOCD on disk is no longer valid (because the zip has been changed), or the zip file is not\n   * in Zip64 format (There are no values in the EOCD that overflow or any files with Zip64\n   * extended information.)\n   *\n   * <p> If this value is {@code nonnull} then the EOCD exists and is in Zip64 format (<i>i.e.</i>\n   * both {@link #eocdEntry} and {@link #eocd64Entry} will be {@code nonnull}).\n   */\n  @Nullable\n  private FileUseMapEntry<Zip64EocdLocator> eocd64Locator;\n\n  /**\n   * The Zip64 EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new),\n   * the EOCD on disk is no longer valid (because the zip has been changed), or the zip file is not\n   * in Zip64 format (There are no values in the EOCD that overflow or any files with Zip64\n   * extended information.)\n   *\n   * <p> If this value is {@code nonnull} then the EOCD exists and is in Zip64 format (<i>i.e.</i>\n   * both {@link #eocdEntry} and {@link #eocd64Locator} will be {@code nonnull}).\n   */\n  @Nullable\n  private FileUseMapEntry<Zip64Eocd> eocd64Entry;\n\n  /**\n   * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. This\n   * may happen, for example, if the zip has been changed and the Central Directory and EOCD have\n   * been deleted (in-memory). In that case, this field will save the comment to place on the EOCD\n   * once it is created.\n   *\n   * <p>This field will only be non-{@code null} if there is no in-memory EOCD structure\n   * (<i>i.e.</i>, {@link #eocdEntry} is {@code null}, If there is an {@link #eocdEntry}, then the\n   * comment will be there instead of being in this field.\n   */\n  @Nullable\n  private byte[] eocdComment;\n\n  /**\n   * This field contains the extensible data sector in the zip's Zip64 EOCD if there is no EOCD\n   * in-memory. This may happen if the zip has been modified and the Central Directory and EOCD have\n   * been deleted (in-memory). In that case, this field will save the data sector to place in the\n   * Zip64 EOCD once it is created.\n   *\n   * <p>This field will only be non-{@code null} if there is no in-memory EOCD structure\n   * (<i>i.e.</i>, {@link #eocdEntry} is {@code null}, If there is an {@link #eocdEntry}, then the\n   * data sector will be in the {@link #eocd64Entry} instead of being in this field.\n   */\n  @Nullable\n  private Zip64ExtensibleDataSector eocdDataSector;\n\n  /**\n   * Specifies whether the Zip64 Eocd will be in Version 2 or Version 1 format when it is\n   * constructed.\n   */\n  private boolean useVersion2Header;\n\n  /** The zip file to which this EOCD record belongs. */\n  private final ZFile file;\n\n  /** The in-memory map of the pieces of the zip-file. */\n  private final FileUseMap map;\n\n  /** The zip file's log. */\n  private final VerifyLog verifyLog;\n\n  /**\n   * Constructs an empty EOCD group, which will have no in-memory EOCD structure.\n   *\n   * @param file The zip file to which this EOCD record belongs.\n   * @param map he in-memory map of the zip file.\n   */\n  EocdGroup(ZFile file, FileUseMap map) {\n\n    eocd64Entry = null;\n    eocd64Locator = null;\n    eocdEntry = null;\n    eocdComment = new byte[0];\n    eocdDataSector = new Zip64ExtensibleDataSector();\n    this.file = file;\n    this.map = map;\n    this.verifyLog = file.getVerifyLog();\n    useVersion2Header = false;\n  }\n\n  /**\n   * Attempts to read the EOCD record into the {@link EocdGroup} from disk specified by\n   * {@link #file}. It will populate the in-memory EOCD structure (<i>i.e.</i> {@link #eocdEntry}),\n   * including the Zip64 EOCD record and locator if applicable.\n   *\n   * @param fileLength The length of the file on disk, used to help find the EOCD record.\n   * @throws IOException Failed to read the EOCD.\n   */\n  void readRecord(long fileLength) throws IOException {\n    /*\n     * Read the last part of the zip into memory. If we don't find the EOCD signature by then,\n     * the file is corrupt.\n     */\n    int lastToRead = LAST_BYTES_TO_READ;\n    if (lastToRead > fileLength) {\n      lastToRead = Ints.checkedCast(fileLength);\n    }\n\n    byte[] last = new byte[lastToRead];\n    file.directFullyRead(fileLength - lastToRead, last);\n\n    /*\n     * Start endIdx at the first possible location where the signature can be located and then\n     * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the\n     * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE.\n     *\n     * Because the EOCD signature may exist in the file comment, when we find a signature we\n     * will try to read the Eocd. If we fail, we continue searching for the signature. However,\n     * we will keep the last exception in case we don't find any signature.\n     */\n    Eocd eocd = null;\n    int foundEocdSignatureIdx = -1;\n    IOException errorFindingSignature = null;\n    long eocdStart = -1;\n\n    for (int endIdx = last.length - MIN_EOCD_SIZE;\n        endIdx >= 0 && foundEocdSignatureIdx == -1;\n        endIdx--) {\n\n      ByteBuffer potentialLocator = ByteBuffer.wrap(last, endIdx, 4);\n      if (LittleEndianUtils.readUnsigned4Le(potentialLocator) == EOCD_SIGNATURE) {\n\n        /*\n         * We found a signature. Try to read the EOCD record.\n         */\n\n        foundEocdSignatureIdx = endIdx;\n        ByteBuffer eocdBytes =\n            ByteBuffer.wrap(last, foundEocdSignatureIdx, last.length - foundEocdSignatureIdx);\n\n        try {\n          eocd = new Eocd(eocdBytes);\n\n          eocdStart = fileLength - lastToRead + foundEocdSignatureIdx;\n\n          /*\n           * Make sure the EOCD takes the whole file up to the end. Log an error if it\n           * doesn't.\n           */\n          if (eocdStart + eocd.getEocdSize() != fileLength) {\n            verifyLog.log(\n                \"EOCD starts at \"\n                    + eocdStart\n                    + \" and has \"\n                    + eocd.getEocdSize()\n                    + \" bytes, but file ends at \"\n                    + fileLength\n                    + \".\");\n          }\n        } catch (IOException e) {\n          if (errorFindingSignature != null) {\n            e.addSuppressed(errorFindingSignature);\n          }\n\n          errorFindingSignature = e;\n          foundEocdSignatureIdx = -1;\n          eocd = null;\n        }\n      }\n    }\n\n    if (foundEocdSignatureIdx == -1) {\n      throw new IOException(\n          \"EOCD signature not found in the last \" + lastToRead + \" bytes of the file.\",\n          errorFindingSignature);\n    }\n\n    Verify.verify(eocdStart >= 0);\n    eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);\n\n    /*\n     * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64\n     * file and we need to read both the Zip64 EOCD locator and Zip64 EOCD\n     */\n    long zip64LocatorStart = eocdStart - Zip64EocdLocator.LOCATOR_SIZE;\n    if (zip64LocatorStart >= 0) {\n      byte[] possibleZip64Locator = new byte[Zip64EocdLocator.LOCATOR_SIZE];\n      file.directFullyRead(zip64LocatorStart, possibleZip64Locator);\n      if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator))\n          == ZIP64_EOCD_LOCATOR_SIGNATURE) {\n\n        /* found the locator. Read it into memory. */\n\n        Zip64EocdLocator locator = new Zip64EocdLocator(ByteBuffer.wrap(possibleZip64Locator));\n        eocd64Locator = map.add(\n            zip64LocatorStart, zip64LocatorStart + locator.getSize(), locator);\n\n        /* Find the size of the Zip64 EOCD by reading its size field */\n        byte[] zip64EocdSizeHolder = new byte[8];\n        file.directFullyRead(\n            locator.getZ64EocdOffset() + Zip64Eocd.SIZE_OFFSET, zip64EocdSizeHolder);\n        long zip64EocdSize =\n            LittleEndianUtils.readUnsigned8Le(ByteBuffer.wrap(zip64EocdSizeHolder))\n                + Zip64Eocd.TRUE_SIZE_DIFFERENCE;\n\n        /* read the Zip64 EOCD into memory */\n\n        byte[] zip64EocdBytes = new byte[Ints.checkedCast(zip64EocdSize)];\n        file.directFullyRead(locator.getZ64EocdOffset(), zip64EocdBytes);\n        Zip64Eocd zip64Eocd = new Zip64Eocd(ByteBuffer.wrap(zip64EocdBytes));\n        useVersion2Header =\n            zip64Eocd.getVersionToExtract()\n                >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION;\n\n        long zip64EocdEnd = locator.getZ64EocdOffset() + zip64EocdSize;\n        if (zip64EocdEnd != zip64LocatorStart) {\n          String msg =\n              \"Zip64 EOCD record is stored in [\"\n                  + locator.getZ64EocdOffset()\n                  + \" - \"\n                  + zip64EocdEnd\n                  + \"] and EOCD starts at \"\n                  + zip64LocatorStart\n                  + \".\";\n\n          /*\n           * If there is an empty space between the Zip64 EOCD and the EOCD locator, we proceed\n           * logging an error. If the Zip64 EOCD ends after the start of the EOCD locator (and\n           * therefore, they overlap), throw an exception.\n           */\n          if (zip64EocdEnd > zip64LocatorStart) {\n            throw new IOException(msg);\n          } else {\n            verifyLog.log(msg);\n          }\n        }\n\n        eocd64Entry = map.add(\n            locator.getZ64EocdOffset(), zip64EocdEnd, zip64Eocd);\n      }\n    }\n\n  }\n\n  /**\n   * Computes the EOCD record from the given Central Directory entry in memory. This will populate\n   * the EOCD in-memory and possibly the Zip64 EOCD and Locator if applicable.\n   *\n   * @param directoryEntry The entry to create the EOCD record from.\n   * @param extraDirectoryOffset The offset between the last local entry and the Central Directory.\n   * This will be preserved by the EOCD if the Central Directory is empty.\n   * @throws IOException Failed to create the EOCD record.\n   */\n  void computeRecord(\n      @Nullable FileUseMapEntry<CentralDirectory> directoryEntry,\n      long extraDirectoryOffset) throws IOException {\n\n    long dirStart;\n    long dirSize;\n    long dirNumEntries;\n\n    if (directoryEntry != null) {\n      dirStart = directoryEntry.getStart();\n      dirSize = directoryEntry.getSize();\n      dirNumEntries = directoryEntry.getStore().getEntries().size();\n    } else {\n      // if we do not have a directory, then we must leave any required offset.\n      dirStart = extraDirectoryOffset;\n      dirSize = 0;\n      dirNumEntries = 0;\n    }\n\n    /*\n     * We need a Zip64 EOCD if any value overflows or if Zip64 file extensions are used as stated\n     * in the Zip Specification.\n     */\n\n    boolean useZip64Eocd =\n        dirStart > Eocd.MAX_CD_OFFSET ||\n            dirSize > Eocd.MAX_CD_SIZE ||\n            dirNumEntries > Eocd.MAX_TOTAL_RECORDS ||\n            (directoryEntry != null && directoryEntry.getStore().containsZip64Files());\n\n    /* construct the Zip64 EOCD and locator first, as they come before the standard EOCD */\n    if (useZip64Eocd) {\n      Verify.verify(eocdDataSector != null);\n      Zip64Eocd zip64Eocd =\n          new Zip64Eocd(dirNumEntries, dirStart, dirSize, useVersion2Header, eocdDataSector);\n      eocdDataSector = null;\n      byte[] zip64EocdBytes = zip64Eocd.toBytes();\n      long zip64Offset = map.size();\n      map.extend(zip64Offset + zip64EocdBytes.length);\n      eocd64Entry = map.add(zip64Offset, zip64Offset + zip64EocdBytes.length, zip64Eocd);\n\n      Zip64EocdLocator locator = new Zip64EocdLocator(eocd64Entry.getStart());\n      byte[] locatorBytes = locator.toBytes();\n      long locatorOffset = map.size();\n      map.extend(locatorOffset + locatorBytes.length);\n      eocd64Locator = map.add(locatorOffset, locatorOffset + locatorBytes.length, locator);\n    }\n\n    /* add the EOCD to the end of the file */\n\n    Verify.verify(eocdComment != null);\n    Eocd eocd = new Eocd(\n        Math.min(dirNumEntries, Eocd.MAX_TOTAL_RECORDS),\n        Math.min(dirStart, Eocd.MAX_CD_OFFSET),\n        Math.min(dirSize, Eocd.MAX_CD_SIZE),\n        eocdComment);\n    eocdComment = null;\n    byte[] eocdBytes = eocd.toBytes();\n    long eocdOffset = map.size();\n    map.extend(eocdOffset + eocdBytes.length);\n    eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);\n  }\n\n  /**\n   * Writes the entire EOCD record to the end of the file. The EOCDGroup must <i>not</i> be empty\n   * ({@link #isEmpty()}) by being populated by a call to\n   * {@link #computeRecord(FileUseMapEntry, long)}, and the Central Directory must already be\n   * written to the file. If the CentralDirectory has not written, then {@link #file} should have\n   * no entries.\n   *\n   * @throws IOException Failed to write the EOCD record.\n   */\n  void appendToFile() throws IOException {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    if (eocd64Entry != null) {\n      Zip64Eocd zip64Eocd = eocd64Entry.getStore();\n      Preconditions.checkNotNull(zip64Eocd);\n      Zip64EocdLocator locator = eocd64Locator.getStore();\n      Preconditions.checkNotNull(locator);\n\n      file.directWrite(eocd64Entry.getStart(), zip64Eocd.toBytes());\n      file.directWrite(eocd64Locator.getStart(), locator.toBytes());\n    }\n\n    Eocd eocd = eocdEntry.getStore();\n    Preconditions.checkNotNull(eocd, \"eocd == null\");\n\n    byte[] eocdBytes = eocd.toBytes();\n    long eocdOffset = eocdEntry.getStart();\n\n    file.directWrite(eocdOffset, eocdBytes);\n  }\n\n  /**\n   * Obtains the byte array representation of the EOCD. The EOCD must have already been computed for\n   * this method to be invoked.\n   *\n   * @return The byte representation of the EOCD.\n   * @throws IOException Failed to obtain the byte representation of the EOCD.\n   */\n  byte[] getEocdBytes() throws IOException {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    Eocd eocd = eocdEntry.getStore();\n    Preconditions.checkNotNull(eocd, \"eocd == null\");\n\n    return eocd.toBytes();\n  }\n\n  /**\n   * Obtains the byte array representation of the Zip64 EOCD Locator. The EOCD record must already\n   * have been computed for this method to be invoked.\n   *\n   * @return The byte representation of the Zip64 EOCD Locator, or null if the EOCD record is not\n   * in Zip64 format.\n   * @throws IOException Failed to obtain the byte representation of the EOCD Locator.\n   */\n  @VisibleForTesting\n  @Nullable\n  byte[] getEocdLocatorBytes() throws IOException {\n    Preconditions.checkNotNull(eocdEntry);\n\n    if (eocd64Locator == null) {\n      return null;\n    }\n\n    return eocd64Locator.getStore().toBytes();\n  }\n\n  /**\n   * Obtains the byte array representation of the Zip64 EOCD. The EOCD record must already\n   * have been computed for this method to be invoked.\n   *\n   * @return The byte representation of the Zip64 EOCD, or null if the EOCD record is not\n   * in Zip64 format.\n   * @throws IOException Failed to obtain the byte representation of the Zip64 EOCD.\n   */\n  @VisibleForTesting\n  @Nullable\n  byte[] getZ64EocdBytes() throws IOException {\n    Preconditions.checkNotNull(eocdEntry);\n\n    if (eocd64Entry == null) {\n      return null;\n    }\n\n    return eocd64Entry.getStore().toBytes();\n  }\n\n  /**\n   * Checks whether the EOCD record is presently in-memory. (<i>i.e.</i> the EOCD was either read\n   * from disk and is still valid, or has been computed from the Central Directory).\n   *\n   * @return True iff the EOCD record is in-memory.\n   */\n  boolean isEmpty() {\n    return eocdEntry == null;\n  }\n\n  /**\n   * Sets whether or not the EOCD record should use the Version 1 or Version 2 of the Zip64 EOCD\n   * (iff the file needs a Zip64 record). The EOCD record should not be in-memory when trying to set\n   * this value, and the EOCD will need to be recomputed to have any affect.\n   *\n   * @param useVersion2Header True if the Version 2 header is to be used, and false for the Version\n   * 1 header.\n   */\n  void setUseVersion2Header(boolean useVersion2Header) {\n    verifyLog.verify(eocdEntry == null, \"eocdEntry != null\");\n\n    this.useVersion2Header = useVersion2Header;\n  }\n\n  /**\n   * Specifies if the EOCD Group will be using a Version 2 Zip64 EOCD record or a Version 1 record\n   * if the file needs to be in Zip64 format.\n   *\n   * @return True if the Version 2 record will be used, and false if the Version 1 record will be\n   * used.\n   */\n  boolean usingVersion2Header() {\n    return useVersion2Header;\n  }\n\n  /**\n   * Removes the EOCD record from memory.\n   */\n  void deleteRecord() {\n    if (eocdEntry != null) {\n      map.remove(eocdEntry);\n\n      Eocd eocd = eocdEntry.getStore();\n      Verify.verify(eocd != null);\n      eocdComment = eocd.getComment();\n      eocdEntry = null;\n    }\n\n    if (eocd64Locator != null) {\n      Verify.verify(eocd64Entry != null);\n      eocdDataSector = eocd64Entry.getStore().getExtraFields();\n      map.remove(eocd64Locator);\n      map.remove(eocd64Entry);\n      eocd64Locator = null;\n      eocd64Entry = null;\n    } else {\n      eocdDataSector = new Zip64ExtensibleDataSector();\n    }\n  }\n\n  /**\n   * Sets the EOCD comment.\n   *\n   * @param comment The new comment; no conversion is done, these exact bytes will be placed in the\n   *     EOCD comment.\n   * @throws IllegalArgumentException If the comment corrupts the ZipFile by having a valid EOCD\n   *     record in it.\n   */\n  void setEocdComment(byte[] comment) {\n    if (comment.length > MAX_EOCD_COMMENT_SIZE) {\n      throw new IllegalArgumentException(\n          \"EOCD comment size (\"\n              + comment.length\n              + \") is larger than the maximum allowed (\"\n              + MAX_EOCD_COMMENT_SIZE\n              + \")\");\n    }\n\n    // Check if the EOCD signature appears anywhere in the comment we need to check if it\n    // is valid.\n    for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) {\n      // Remember: little endian...\n      ByteBuffer potentialSignature = ByteBuffer.wrap(comment, i, 4);\n      try {\n        if (LittleEndianUtils.readUnsigned4Le(potentialSignature) == EOCD_SIGNATURE) {\n          // We found a possible EOCD signature at position i. Try to read it.\n          ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i);\n          try {\n            new Eocd(bytes);\n            // If a valid record is found in the comment then this corrupts the Zip file record\n            // as we look for the EOCD at the back of the file (where the comment is) first.\n            throw new IllegalArgumentException(\n                \"Position \" + i + \" of the comment contains a valid EOCD record.\");\n          } catch (IOException e) {\n            // Fine, this is an invalid record. Move along...\n          }\n        }\n      } catch (IOException e) {\n        throw new IOExceptionWrapper(e);\n      }\n    }\n\n    deleteRecord();\n    eocdComment = new byte[comment.length];\n    System.arraycopy(comment, 0, eocdComment, 0, comment.length);\n  }\n\n  /**\n   * Returns the start of the EOCD record location in the file or -1 if the EOCD is not in memory.\n   *\n   * @return The start of the record.\n   */\n  long getOffset() {\n    if (eocdEntry == null) {\n      return -1;\n    }\n    return getRecordStart();\n  }\n\n  /**\n   * Gets the comment in the EOCD.\n   *\n   * @return The comment exactly as it was encoded in the EOCD, no encoding is done.\n   */\n  byte[] getEocdComment() {\n    if (eocdEntry == null) {\n      Verify.verify(eocdComment != null);\n      byte[] eocdCommentCopy = eocdComment.clone();\n      return eocdCommentCopy;\n    }\n\n    Eocd eocd = eocdEntry.getStore();\n    Verify.verify(eocd != null);\n    return eocd.getComment();\n  }\n\n  /**\n   * Gets the size of the central directory as specified from the EOCD record. The EOCD must be in\n   * memory before this method is invoked.\n   *\n   * @return The directory's size.\n   */\n  long getDirectorySize() {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    Eocd eocd = eocdEntry.getStore();\n\n    if (eocd64Entry != null && eocd.getDirectorySize() == Eocd.MAX_CD_SIZE) {\n      return eocd64Entry.getStore().getDirectorySize();\n    } else {\n      return eocd.getDirectorySize();\n    }\n  }\n\n  /**\n   * Gets the offset of the Central Directory from the start of the archive as specified from the\n   * EOCD record. The EOCD must be in memory before this method is invoked.\n   *\n   * @return The offset of the start of the Central Directory.\n   */\n  long getDirectoryOffset() {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    Eocd eocd = eocdEntry.getStore();\n\n    if (eocd64Entry != null && eocd.getDirectoryOffset() == Eocd.MAX_CD_OFFSET) {\n      return eocd64Entry.getStore().getDirectoryOffset();\n    } else {\n      return eocd.getDirectoryOffset();\n    }\n  }\n\n  /**\n   * Gets the total number of entries in the Central Directory as specified from the EOCD record.\n   * The EOCD must be in memory before this method is invoked.\n   *\n   * @return The total number of records in the Central Directory.\n   */\n  long getTotalDirectoryRecords() {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    Eocd eocd = eocdEntry.getStore();\n    if (eocd64Entry != null && eocd.getTotalRecords() == Eocd.MAX_TOTAL_RECORDS) {\n      return eocd64Entry.getStore().getTotalRecords();\n    }\n\n    return eocd.getTotalRecords();\n  }\n\n  /**\n   * Returns the start of the EOCD record from the start of the archive. This will be the same as\n   * the start of the standard EOCD in a Zip32 file or in a Zip64 file will be the start of the\n   * Zip64 Eocd record. The EOCD must be in memory for this method to be invoked.\n   *\n   * @return The start of the entire EOCD record.\n   */\n  long getRecordStart() {\n    Verify.verify(eocdEntry != null, \"eocdEntry == null\");\n    if (eocd64Entry != null) {\n      return eocd64Entry.getStart();\n    }\n    return eocdEntry.getStart();\n  }\n\n  /**\n   * Returns the total size of the EOCD record. This will be the same as the standard EOCD size for\n   * a Zip32 file or in a Zip64 file will be the start of the Zip64 record to the end of the\n   * standard EOCD. the EOCD must be in memory for this method to be invoked.\n   *\n   * @return The total size of the EOCD record.\n   */\n  public long getRecordSize() {\n    if (eocd64Entry != null) {\n      Verify.verify(eocdEntry != null);\n      return eocdEntry.getEnd() - eocd64Entry.getStart();\n    }\n    if (eocdEntry == null) {\n      return -1;\n    }\n\n    return eocdEntry.getSize();\n  }\n\n  /**\n   * Returns the Zip64 Extensible Data Sector, or {@code null} if the EOCD record is not in the\n   * Zip64 format. The EOCD must be in memory for this method to be invoked.\n   *\n   * @return The Extensible data sector, or {@code null} if none exists.\n   */\n  @Nullable\n  public Zip64ExtensibleDataSector getExtensibleData() {\n    Verify.verify(eocdEntry != null);\n    if (eocd64Entry != null) {\n      return eocd64Entry.getStore().getExtraFields();\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ExtraField.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableList;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.annotation.Nullable;\n\n/**\n * Contains an extra field.\n *\n * <p>According to the zip specification, the extra field is composed of a sequence of fields. This\n * class provides a way to access, parse and modify that information.\n *\n * <p>The zip specification calls fields to the fields inside the extra field. Because this\n * terminology is confusing, we use <i>segment</i> to refer to a part of the extra field. Each\n * segment is represented by an instance of {@link Segment} and contains a header ID and data.\n *\n * <p>Each instance of {@link ExtraField} is immutable. The extra field of a particular entry can be\n * changed by creating a new instanceof {@link ExtraField} and pass it to {@link\n * StoredEntry#setLocalExtra(ExtraField)}.\n *\n * <p>Instances of {@link ExtraField} can be created directly from the list of segments in it or\n * from the raw byte data. If created from the raw byte data, the data will only be parsed on\n * demand. So, if neither {@link #getSegments()} nor {@link #getSingleSegment(int)} is invoked, the\n * extra field will not be parsed. This guarantees low performance impact of the using the extra\n * field unless its contents are needed.\n */\npublic class ExtraField {\n  public static final ExtraField EMPTY = new ExtraField();\n\n  /** Header ID for field with zip alignment. */\n  static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935;\n\n  /** Header ID for field with linking entry. */\n  static final int LINKING_ENTRY_EXTRA_DATA_FIELD_HEADER_ID = 0x2333;\n\n  /**\n   * The field's raw data, if it is known. Either this variable or {@link #segments} must be\n   * non-{@code null}.\n   */\n  @Nullable private final byte[] rawData;\n\n  /**\n   * The list of field's segments. Will be populated if the extra field is created based on a list\n   * of segments; will also be populated after parsing if the extra field is created based on the\n   * raw bytes.\n   */\n  @Nullable private ImmutableList<Segment> segments;\n\n  /**\n   * Creates an extra field based on existing raw data.\n   *\n   * @param rawData the raw data; will not be parsed unless needed\n   */\n  public ExtraField(byte[] rawData) {\n    this.rawData = rawData;\n    segments = null;\n  }\n\n  /** Creates a new extra field with no segments. */\n  public ExtraField() {\n    rawData = null;\n    segments = ImmutableList.of();\n  }\n\n  /**\n   * Creates a new extra field with the given segments.\n   *\n   * @param segments the segments\n   */\n  public ExtraField(ImmutableList<Segment> segments) {\n    rawData = null;\n    this.segments = segments;\n  }\n\n  /**\n   * Obtains all segments in the extra field.\n   *\n   * @return all segments\n   * @throws IOException failed to parse the extra field\n   */\n  public ImmutableList<Segment> getSegments() throws IOException {\n    if (segments == null) {\n      parseSegments();\n    }\n\n    Preconditions.checkNotNull(segments);\n    return segments;\n  }\n\n  /**\n   * Obtains the only segment with the provided header ID.\n   *\n   * @param headerId the header ID\n   * @return the segment found or {@code null} if no segment contains the provided header ID\n   * @throws IOException there is more than one header with the provided header ID\n   */\n  @Nullable\n  public Segment getSingleSegment(int headerId) throws IOException {\n    List<Segment> found = new ArrayList<>();\n    for (Segment s : getSegments()) {\n      if (s.getHeaderId() == headerId) {\n        found.add(s);\n      }\n    }\n\n    if (found.isEmpty()) {\n      return null;\n    } else if (found.size() == 1) {\n      return found.get(0);\n    } else {\n      throw new IOException(found.size() + \" segments with header ID \" + headerId + \"found\");\n    }\n  }\n\n  /**\n   * Parses the raw data and generates all segments in {@link #segments}.\n   *\n   * @throws IOException failed to parse the data\n   */\n  private void parseSegments() throws IOException {\n    Preconditions.checkNotNull(rawData);\n    Preconditions.checkState(segments == null);\n\n    List<Segment> segments = new ArrayList<>();\n    ByteBuffer buffer = ByteBuffer.wrap(rawData);\n\n    while (buffer.remaining() > 0) {\n      int headerId = LittleEndianUtils.readUnsigned2Le(buffer);\n      int dataSize = LittleEndianUtils.readUnsigned2Le(buffer);\n      if (dataSize < 0) {\n        throw new IOException(\n            \"Invalid data size for extra field segment with header ID \"\n                + headerId\n                + \": \"\n                + dataSize);\n      }\n\n      byte[] data = new byte[dataSize];\n      if (buffer.remaining() < dataSize) {\n        throw new IOException(\n            \"Invalid data size for extra field segment with header ID \"\n                + headerId\n                + \": \"\n                + dataSize\n                + \" (only \"\n                + buffer.remaining()\n                + \" bytes are available)\");\n      }\n      buffer.get(data);\n\n      SegmentFactory factory = identifySegmentFactory(headerId);\n      Segment seg = factory.make(headerId, data);\n      segments.add(seg);\n    }\n\n    this.segments = ImmutableList.copyOf(segments);\n  }\n\n  /**\n   * Obtains the size of the extra field.\n   *\n   * @return the size\n   */\n  public int size() {\n    if (rawData != null) {\n      return rawData.length;\n    } else {\n      Preconditions.checkNotNull(segments);\n      int sz = 0;\n      for (Segment s : segments) {\n        sz += s.size();\n      }\n\n      return sz;\n    }\n  }\n\n  /**\n   * Writes the extra field to the given output buffer.\n   *\n   * @param out the output buffer to write the field; exactly {@link #size()} bytes will be written\n   * @throws IOException failed to write the extra fields\n   */\n  public void write(ByteBuffer out) throws IOException {\n    if (rawData != null) {\n      out.put(rawData);\n    } else {\n      Preconditions.checkNotNull(segments);\n      for (Segment s : segments) {\n        s.write(out);\n      }\n    }\n  }\n\n  /**\n   * Identifies the factory to create the segment with the provided header ID.\n   *\n   * @param headerId the header ID\n   * @return the segmnet factory that creates segments with the given header\n   */\n  private static SegmentFactory identifySegmentFactory(int headerId) {\n    if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {\n      return AlignmentSegment::new;\n    }\n\n    return RawDataSegment::new;\n  }\n\n  /**\n   * Field inside the extra field. A segment contains a header ID and data. Specific types of\n   * segments implement this interface.\n   */\n  public interface Segment {\n\n    /**\n     * Obtains the segment's header ID.\n     *\n     * @return the segment's header ID\n     */\n    int getHeaderId();\n\n    /**\n     * Obtains the size of the segment including the header ID.\n     *\n     * @return the number of bytes needed to write the segment\n     */\n    int size();\n\n    /**\n     * Writes the segment to a buffer.\n     *\n     * @param out the buffer where to write the segment to; exactly {@link #size()} bytes will be\n     *     written\n     * @throws IOException failed to write segment data\n     */\n    void write(ByteBuffer out) throws IOException;\n  }\n\n  /** Factory that creates a segment. */\n  interface SegmentFactory {\n\n    /**\n     * Creates a new segment.\n     *\n     * @param headerId the header ID\n     * @param data the segment's data\n     * @return the created segment\n     * @throws IOException failed to create the segment from the data\n     */\n    Segment make(int headerId, byte[] data) throws IOException;\n  }\n\n  /**\n   * Segment of raw data: this class represents a general segment containing an array of bytes as\n   * data.\n   */\n  public static class RawDataSegment implements Segment {\n\n    /** Header ID. */\n    private final int headerId;\n\n    /** Data in the segment. */\n    private final byte[] data;\n\n    /**\n     * Creates a new raw data segment.\n     *\n     * @param headerId the header ID\n     * @param data the segment data\n     */\n    RawDataSegment(int headerId, byte[] data) {\n      this.headerId = headerId;\n      this.data = data;\n    }\n\n    @Override\n    public int getHeaderId() {\n      return headerId;\n    }\n\n    @Override\n    public void write(ByteBuffer out) throws IOException {\n      LittleEndianUtils.writeUnsigned2Le(out, headerId);\n      LittleEndianUtils.writeUnsigned2Le(out, data.length);\n      out.put(data);\n    }\n\n    @Override\n    public int size() {\n      return 4 + data.length;\n    }\n  }\n\n  /**\n   * Segment with information on an alignment: this segment contains information on how an entry\n   * should be aligned and contains zero-filled data to force alignment.\n   *\n   * <p>An alignment segment contains the header ID, the size of the data, the alignment value and\n   * zero bytes to pad\n   */\n  public static class AlignmentSegment implements Segment {\n\n    /** Minimum size for an alignment segment. */\n    public static final int MINIMUM_SIZE = 6;\n\n    /** The alignment value. */\n    private int alignment;\n\n    /** How many bytes of padding are in this segment? */\n    private int padding;\n\n    /**\n     * Creates a new alignment segment.\n     *\n     * @param alignment the alignment value\n     * @param totalSize how many bytes should this segment take?\n     */\n    public AlignmentSegment(int alignment, int totalSize) {\n      Preconditions.checkArgument(alignment > 0, \"alignment <= 0\");\n      Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, \"totalSize < MINIMUM_SIZE\");\n\n      /*\n       * We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment\n       * value (2 bytes).\n       */\n      this.alignment = alignment;\n      padding = totalSize - MINIMUM_SIZE;\n    }\n\n    /**\n     * Creates a new alignment segment from extra data.\n     *\n     * @param headerId the header ID\n     * @param data the segment data\n     * @throws IOException failed to create the segment from the data\n     */\n    public AlignmentSegment(int headerId, byte[] data) throws IOException {\n      Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);\n\n      ByteBuffer dataBuffer = ByteBuffer.wrap(data);\n      alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);\n      if (alignment <= 0) {\n        throw new IOException(\"Invalid alignment in alignment field: \" + alignment);\n      }\n\n      padding = data.length - 2;\n    }\n\n    @Override\n    public void write(ByteBuffer out) throws IOException {\n      LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);\n      LittleEndianUtils.writeUnsigned2Le(out, padding + 2);\n      LittleEndianUtils.writeUnsigned2Le(out, alignment);\n      out.put(new byte[padding]);\n    }\n\n    @Override\n    public int size() {\n      return padding + 6;\n    }\n\n    @Override\n    public int getHeaderId() {\n      return ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID;\n    }\n  }\n\n  public static class LinkingEntrySegment implements Segment {\n\n    private final StoredEntry linkingEntry;\n    private int dataOffset = -1;\n    private long zipOffset = -1;\n\n    public LinkingEntrySegment(StoredEntry linkingEntry) throws IOException {\n      Preconditions.checkArgument(linkingEntry.isLinkingEntry(), \"linkingEntry is not a linking entry\");\n      this.linkingEntry = linkingEntry;\n    }\n\n    @Override\n    public int getHeaderId() {\n      return LINKING_ENTRY_EXTRA_DATA_FIELD_HEADER_ID;\n    }\n\n    @Override\n    public int size() {\n      return linkingEntry.isDummyEntry() ? 0 : linkingEntry.getLocalHeaderSize() + 4;\n    }\n\n    public void setOffset(int dataOffset, long zipOffset) {\n      this.dataOffset = dataOffset;\n      this.zipOffset = zipOffset;\n    }\n\n    @Override\n    public void write(ByteBuffer out) throws IOException {\n      if (dataOffset < 0 || zipOffset < 0) {\n        throw new IOException(\"linking entry has wrong offset\");\n      }\n      if (!linkingEntry.isDummyEntry()) {\n        LittleEndianUtils.writeUnsigned2Le(out, LINKING_ENTRY_EXTRA_DATA_FIELD_HEADER_ID);\n        LittleEndianUtils.writeUnsigned2Le(out, linkingEntry.getLocalHeaderSize());\n        var offset = out.position();\n        linkingEntry.writeData(out, dataOffset - linkingEntry.getLocalHeaderSize() - offset);\n        linkingEntry.replaceSourceFromZip(offset + zipOffset);\n      } else {\n        linkingEntry.replaceSourceFromZip(zipOffset + dataOffset + linkingEntry.getNestedOffset());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMap.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Sets;\nimport com.google.common.primitives.Ints;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.SortedSet;\nimport java.util.TreeSet;\nimport javax.annotation.Nullable;\n\n/**\n * The file use map keeps track of which parts of the zip file are used which parts are not. It\n * essentially maintains an ordered set of entries ({@link FileUseMapEntry}). Each entry either has\n * some data (an entry, the Central Directory, the EOCD) or is a free entry.\n *\n * <p>For example: [0-95, \"foo/\"][95-260, \"xpto\"][260-310, free][310-360, Central Directory]\n * [360-390,EOCD]\n *\n * <p>There are a few invariants in this structure:\n *\n * <ul>\n *   <li>there are no gaps between map entries;\n *   <li>the map is fully covered up to its size;\n *   <li>there are no two free entries next to each other; this is guaranteed by coalescing the\n *       entries upon removal (see {@link #coalesce(FileUseMapEntry)});\n *   <li>all free entries have a minimum size defined in the constructor, with the possible\n *       exception of the last one\n * </ul>\n */\nclass FileUseMap {\n  /**\n   * Size of the file according to the map. This should always match the last entry in {@code #map}.\n   */\n  private long size;\n\n  /**\n   * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}. If\n   * {@link #size} is zero then this set is empty. This is the only situation in which the map will\n   * be empty.\n   */\n  private final TreeSet<FileUseMapEntry<?>> map;\n\n  /**\n   * Tree with all free blocks ordered by size. This is essentially a view over {@link #map}\n   * containing only the free blocks, but in a different order.\n   */\n  private final TreeSet<FileUseMapEntry<?>> freeBySize;\n  private final TreeSet<FileUseMapEntry<?>> freeByStart;\n\n  /** If defined, defines the minimum size for a free entry. */\n  private int mMinFreeSize;\n\n  /**\n   * Creates a new, empty file map.\n   *\n   * @param size the size of the file\n   * @param minFreeSize minimum size of a free entry\n   */\n  FileUseMap(long size, int minFreeSize) {\n    Preconditions.checkArgument(size >= 0, \"size < 0\");\n    Preconditions.checkArgument(minFreeSize >= 0, \"minFreeSize < 0\");\n\n    this.size = size;\n    map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);\n    freeBySize = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE);\n    freeByStart = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);\n    mMinFreeSize = minFreeSize;\n\n    if (size > 0) {\n      internalAdd(FileUseMapEntry.makeFree(0, size));\n    }\n  }\n\n  /**\n   * Adds an entry to the internal structures.\n   *\n   * @param entry the entry to add\n   */\n  private void internalAdd(FileUseMapEntry<?> entry) {\n    map.add(entry);\n\n    if (entry.isFree()) {\n      freeBySize.add(entry);\n      freeByStart.add(entry);\n    }\n  }\n\n  /**\n   * Removes an entry from the internal structures.\n   *\n   * @param entry the entry to remove\n   */\n  private void internalRemove(FileUseMapEntry<?> entry) {\n    boolean wasRemoved = map.remove(entry);\n    Preconditions.checkState(wasRemoved, \"entry not in map\");\n\n    if (entry.isFree()) {\n      freeBySize.remove(entry);\n      freeByStart.remove(entry);\n    }\n  }\n\n  /**\n   * Adds a new file to the map. The interval specified by {@code entry} must fit inside an empty\n   * entry in the map. That entry will be replaced by entry and additional free entries will be\n   * added before and after if needed to make sure no spaces exist on the map.\n   *\n   * @param entry the entry to add\n   */\n  private void add(FileUseMapEntry<?> entry) {\n    Preconditions.checkArgument(entry.getStart() < size, \"entry.getStart() >= size\");\n    Preconditions.checkArgument(entry.getEnd() <= size, \"entry.getEnd() > size\");\n    Preconditions.checkArgument(!entry.isFree(), \"entry.isFree()\");\n\n    FileUseMapEntry<?> container = findContainer(entry);\n    Verify.verify(container.isFree(), \"!container.isFree()\");\n\n    Set<FileUseMapEntry<?>> replacements = split(container, entry);\n    internalRemove(container);\n    for (FileUseMapEntry<?> r : replacements) {\n      internalAdd(r);\n    }\n  }\n\n  /**\n   * Adds a new file to the map. The interval specified by ({@code start}, {@code end}) must fit\n   * inside an empty entry in the map. That entry will be replaced by entry and additional free\n   * entries will be added before and after if needed to make sure no spaces exist on the map.\n   *\n   * <p>The entry cannot extend beyong the end of the map. If necessary, extend the map using {@link\n   * #extend(long)}.\n   *\n   * @param start the start of this entry\n   * @param end the end of the entry\n   * @param store extra data to store with the entry\n   * @param <T> the type of data to store in the entry\n   * @return the new entry\n   */\n  <T> FileUseMapEntry<T> add(long start, long end, T store) {\n    Preconditions.checkArgument(start >= 0, \"start < 0\");\n    Preconditions.checkArgument(end > start, \"end < start\");\n\n    FileUseMapEntry<T> entry = FileUseMapEntry.makeUsed(start, end, store);\n    add(entry);\n    return entry;\n  }\n\n  /**\n   * Removes a file from the map, replacing it with an empty one that is then coalesced with\n   * neighbors (if the neighbors are free).\n   *\n   * @param entry the entry\n   */\n  void remove(FileUseMapEntry<?> entry) {\n    Preconditions.checkState(map.contains(entry), \"!map.contains(entry)\");\n    Preconditions.checkArgument(!entry.isFree(), \"entry.isFree()\");\n\n    internalRemove(entry);\n\n    FileUseMapEntry<?> replacement = FileUseMapEntry.makeFree(entry.getStart(), entry.getEnd());\n    internalAdd(replacement);\n    coalesce(replacement);\n  }\n\n  /**\n   * Finds the entry that fully contains the given one. It is assumed that one exists.\n   *\n   * @param entry the entry whose container we're looking for\n   * @return the container\n   */\n  private FileUseMapEntry<?> findContainer(FileUseMapEntry<?> entry) {\n    FileUseMapEntry container = map.floor(entry);\n    Verify.verifyNotNull(container);\n    Verify.verify(container.getStart() <= entry.getStart());\n    Verify.verify(container.getEnd() >= entry.getEnd());\n\n    return container;\n  }\n\n  /**\n   * Splits a container to add an entry, adding new free entries before and after the provided entry\n   * if needed.\n   *\n   * @param container the container entry, a free entry that is in {@link #map} that that encloses\n   *     {@code entry}\n   * @param entry the entry that will be used to split {@code container}\n   * @return a set of non-overlapping entries that completely covers {@code container} and that\n   *     includes {@code entry}\n   */\n  private static Set<FileUseMapEntry<?>> split(\n      FileUseMapEntry<?> container, FileUseMapEntry<?> entry) {\n    Preconditions.checkArgument(container.isFree(), \"!container.isFree()\");\n\n    long farStart = container.getStart();\n    long start = entry.getStart();\n    long end = entry.getEnd();\n    long farEnd = container.getEnd();\n\n    Verify.verify(farStart <= start, \"farStart > start\");\n    Verify.verify(start < end, \"start >= end\");\n    Verify.verify(farEnd >= end, \"farEnd < end\");\n\n    Set<FileUseMapEntry<?>> result = Sets.newHashSet();\n    if (farStart < start) {\n      result.add(FileUseMapEntry.makeFree(farStart, start));\n    }\n\n    result.add(entry);\n\n    if (end < farEnd) {\n      result.add(FileUseMapEntry.makeFree(end, farEnd));\n    }\n\n    return result;\n  }\n\n  /**\n   * Coalesces a free entry replacing it and neighboring free entries with a single, larger entry.\n   * This method does nothing if {@code entry} does not have free neighbors.\n   *\n   * @param entry the free entry to coalesce with neighbors\n   */\n  private void coalesce(FileUseMapEntry<?> entry) {\n    Preconditions.checkArgument(entry.isFree(), \"!entry.isFree()\");\n\n    FileUseMapEntry<?> prevToMerge = null;\n    long start = entry.getStart();\n    if (start > 0) {\n      /*\n       * See if we have a previous entry to merge with this one.\n       */\n      prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start));\n      Verify.verifyNotNull(prevToMerge);\n      if (!prevToMerge.isFree()) {\n        prevToMerge = null;\n      }\n    }\n\n    FileUseMapEntry<?> nextToMerge = null;\n    long end = entry.getEnd();\n    if (end < size) {\n      /*\n       * See if we have a next entry to merge with this one.\n       */\n      nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1));\n      Verify.verifyNotNull(nextToMerge);\n      if (!nextToMerge.isFree()) {\n        nextToMerge = null;\n      }\n    }\n\n    if (prevToMerge == null && nextToMerge == null) {\n      return;\n    }\n\n    long newStart = start;\n    if (prevToMerge != null) {\n      newStart = prevToMerge.getStart();\n      internalRemove(prevToMerge);\n    }\n\n    long newEnd = end;\n    if (nextToMerge != null) {\n      newEnd = nextToMerge.getEnd();\n      internalRemove(nextToMerge);\n    }\n\n    internalRemove(entry);\n    internalAdd(FileUseMapEntry.makeFree(newStart, newEnd));\n  }\n\n  /** Truncates map removing the top entry if it is free and reducing the map's size. */\n  void truncate() {\n    if (size == 0) {\n      return;\n    }\n\n    /*\n     * Find the last entry.\n     */\n    FileUseMapEntry<?> last = map.last();\n    Verify.verifyNotNull(last, \"last == null\");\n    if (last.isFree()) {\n      internalRemove(last);\n      size = last.getStart();\n    }\n  }\n\n  /**\n   * Obtains the size of the map.\n   *\n   * @return the size\n   */\n  long size() {\n    return size;\n  }\n\n  /**\n   * Obtains the largest used offset in the map. This will be size of the map after truncation.\n   *\n   * @return the size of the file discounting the last block if it is empty\n   */\n  long usedSize() {\n    if (size == 0) {\n      return 0;\n    }\n\n    /*\n     * Find the last entry to see if it is an empty entry. If it is, we need to remove its size\n     * from the returned value.\n     */\n    FileUseMapEntry<?> last = map.last();\n    Verify.verifyNotNull(last, \"last == null\");\n    if (last.isFree()) {\n      return last.getStart();\n    } else {\n      Verify.verify(last.getEnd() == size);\n      return size;\n    }\n  }\n\n  /**\n   * Extends the map to guarantee it has at least {@code size} bytes. If the current size is as\n   * large as {@code size}, this method does nothing.\n   *\n   * @param size the new size of the map that cannot be smaller that the current size\n   */\n  void extend(long size) {\n    Preconditions.checkArgument(size >= this.size, \"size < size\");\n\n    if (this.size == size) {\n      return;\n    }\n\n    FileUseMapEntry<?> newBlock = FileUseMapEntry.makeFree(this.size, size);\n    internalAdd(newBlock);\n\n    this.size = size;\n\n    coalesce(newBlock);\n  }\n\n  /**\n   * Locates a free area in the map with at least {@code size} bytes such that {@code ((start +\n   * alignOffset) % align == 0} and such that the free space before {@code start} is not smaller\n   * than the minimum free entry size. This method will follow the algorithm specified by {@code\n   * alg}.\n   *\n   * <p>If no free contiguous block exists in the map that can hold the provided size then the first\n   * free index at the end of the map is provided. This means that the map may need to be extended\n   * before data can be added.\n   *\n   * @param size the size of the contiguous area requested\n   * @param alignOffset an offset to which alignment needs to be computed (see method description)\n   * @param align alignment at the offset (see method description)\n   * @param alg which algorithm to use\n   * @return the location of the contiguous area; this may be located at the end of the map\n   */\n  long locateFree(long size, long alignOffset, long align, PositionAlgorithm alg) {\n    Preconditions.checkArgument(size > 0, \"size <= 0\");\n\n    FileUseMapEntry<?> minimumSizedEntry = FileUseMapEntry.makeFree(0, size);\n    SortedSet<FileUseMapEntry<?>> matches;\n\n    switch (alg) {\n      case BEST_FIT:\n        matches = freeBySize.tailSet(minimumSizedEntry);\n        break;\n      case FIRST_FIT:\n        matches = freeByStart;\n        break;\n      default:\n        throw new AssertionError();\n    }\n\n    FileUseMapEntry<?> best = null;\n    long bestExtraSize = 0;\n    for (FileUseMapEntry<?> curr : matches) {\n      /*\n       * We don't care about blocks that aren't free.\n       */\n      if (!curr.isFree()) {\n        continue;\n      }\n\n      /*\n       * Compute any extra size we need in this block to make sure we verify the alignment.\n       * There must be a better to do this...\n       */\n      long extraSize;\n      if (align == 0) {\n        extraSize = 0;\n      } else {\n        extraSize = (align - ((curr.getStart() + alignOffset) % align)) % align;\n      }\n\n      /*\n       * We can't leave than mMinFreeSize before. So if the extraSize is less than\n       * mMinFreeSize, we have to increase it by 'align' as many times as needed. For\n       * example, if mMinFreeSize is 20, align 4 and extraSize is 5. We need to increase it\n       * to 21 (5 + 4 * 4)\n       */\n      if (extraSize > 0 && extraSize < mMinFreeSize) {\n        int addAlignBlocks = Ints.checkedCast((mMinFreeSize - extraSize + align - 1) / align);\n        extraSize += addAlignBlocks * align;\n      }\n\n      /*\n       * We don't care about blocks where we don't fit in.\n       */\n      if (curr.getSize() < (size + extraSize)) {\n        continue;\n      }\n\n      /*\n       * We don't care about blocks that leave less than the minimum size after. There are\n       * two exceptions: (1) this is the last block and (2) the next block is free in which\n       * case, after coalescing, the free block with have at least the minimum size.\n       */\n      long emptySpaceLeft = curr.getSize() - (size + extraSize);\n      if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) {\n        FileUseMapEntry<?> next = map.higher(curr);\n        if (next != null && !next.isFree()) {\n          continue;\n        }\n      }\n\n      /*\n       * We don't care about blocks that are bigger than the best so far (otherwise this\n       * wouldn't be a best-fit algorithm).\n       */\n      if (best != null && best.getSize() < curr.getSize()) {\n        continue;\n      }\n\n      best = curr;\n      bestExtraSize = extraSize;\n\n      /*\n       * If we're doing first fit, we don't want to search for a better one :)\n       */\n      if (alg == PositionAlgorithm.FIRST_FIT) {\n        break;\n      }\n    }\n\n    /*\n     * If no entry that could hold size is found, get the first free byte.\n     */\n    long firstFree = this.size;\n    if (best == null && !map.isEmpty()) {\n      FileUseMapEntry<?> last = map.last();\n      if (last.isFree()) {\n        firstFree = last.getStart();\n      }\n    }\n\n    /*\n     * We're done: either we found something or we didn't, in which the new entry needs to\n     * be added to the end of the map.\n     */\n    if (best == null) {\n      long extra = (align - ((firstFree + alignOffset) % align)) % align;\n\n      /*\n       * If adding this entry at the end would create a space smaller than the minimum,\n       * push it for 'align' bytes forward.\n       */\n      if (extra > 0) {\n        if (extra < mMinFreeSize) {\n          extra += align * (((mMinFreeSize - extra) + (align - 1)) / align);\n        }\n      }\n\n      return firstFree + extra;\n    } else {\n      return best.getStart() + bestExtraSize;\n    }\n  }\n\n  /**\n   * Obtains all free areas of the map, excluding any trailing free area.\n   *\n   * @return all free areas, an empty set if there are no free areas; the areas are returned in file\n   *     order, that is, if area {@code x} starts before area {@code y}, then area {@code x} will be\n   *     stored before area {@code y} in the list\n   */\n  List<FileUseMapEntry<?>> getFreeAreas() {\n    List<FileUseMapEntry<?>> freeAreas = Lists.newArrayList();\n\n    for (FileUseMapEntry<?> area : map) {\n      if (area.isFree() && area.getEnd() != size) {\n        freeAreas.add(area);\n      }\n    }\n\n    return freeAreas;\n  }\n\n  /**\n   * Obtains the entry that is located before the one provided.\n   *\n   * @param entry the map entry to get the previous one for; must belong to the map\n   * @return the entry before the provided one, {@code null} if {@code entry} is the first entry in\n   *     the map\n   */\n  @Nullable\n  FileUseMapEntry<?> before(FileUseMapEntry<?> entry) {\n    Preconditions.checkNotNull(entry, \"entry == null\");\n\n    return map.lower(entry);\n  }\n\n  /**\n   * Obtains the entry that is located after the one provided.\n   *\n   * @param entry the map entry to get the next one for; must belong to the map\n   * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in\n   *     the map\n   */\n  @Nullable\n  FileUseMapEntry<?> after(FileUseMapEntry<?> entry) {\n    Preconditions.checkNotNull(entry, \"entry == null\");\n\n    return map.higher(entry);\n  }\n\n  /**\n   * Obtains the entry at the given offset.\n   *\n   * @param offset the offset to look for\n   * @return the entry found or {@code null} if there is no entry (not even a free one) at the given\n   *     offset\n   */\n  @Nullable\n  FileUseMapEntry<?> at(long offset) {\n    Preconditions.checkArgument(offset >= 0, \"offset < 0\");\n    Preconditions.checkArgument(offset < size, \"offset >= size\");\n\n    FileUseMapEntry<?> entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1));\n    if (entry == null) {\n      return null;\n    }\n\n    Verify.verify(entry.getStart() <= offset);\n    Verify.verify(entry.getEnd() > offset);\n\n    return entry;\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder builder = new StringBuilder();\n    boolean first = true;\n    for (FileUseMapEntry<?> entry : map) {\n      if (first) {\n        first = false;\n      } else {\n        builder.append(\", \");\n      }\n\n      builder.append(entry.getStart());\n      builder.append(\" - \");\n      builder.append(entry.getEnd());\n      builder.append(\": \");\n      builder.append(entry.getStore());\n    }\n    return builder.toString();\n  }\n\n  /** Algorithms used to position entries in blocks. */\n  public enum PositionAlgorithm {\n    /** Best fit: finds the smallest free block that can receive the entry. */\n    BEST_FIT,\n\n    /** First fit: finds the first free block that can receive the entry. */\n    FIRST_FIT\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/FileUseMapEntry.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.google.common.base.MoreObjects;\nimport com.google.common.base.Preconditions;\nimport com.google.common.primitives.Ints;\nimport java.util.Comparator;\nimport javax.annotation.Nullable;\n\n/**\n * Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The end\n * of the interval is exclusive.\n *\n * <p>Entries can either be free or used. Used entries <em>must</em> store an object. Free entries\n * do not store anything.\n *\n * <p>File map entries are used to keep track of which parts of a file map are used and not.\n *\n * @param <T> the type of data stored\n */\nclass FileUseMapEntry<T> {\n\n  /** Comparator that compares entries by their start date. */\n  public static final Comparator<FileUseMapEntry<?>> COMPARE_BY_START =\n      (o1, o2) -> Ints.saturatedCast(o1.getStart() - o2.getStart());\n\n  /** Comparator that compares entries by their size. */\n  public static final Comparator<FileUseMapEntry<?>> COMPARE_BY_SIZE =\n      (o1, o2) -> Ints.saturatedCast(o1.getSize() - o2.getSize());\n\n  /** The first byte in the entry. */\n  private final long start;\n\n  /** The first byte no longer in the entry. */\n  private final long end;\n\n  /** The stored data. If {@code null} then this entry represents a free entry. */\n  @Nullable private final T store;\n\n  /**\n   * Creates a new map entry.\n   *\n   * @param start the start of the entry\n   * @param end the end of the entry (first byte no longer in the entry)\n   * @param store the data to store in the entry or {@code null} if this is a free entry\n   */\n  private FileUseMapEntry(long start, long end, @Nullable T store) {\n    Preconditions.checkArgument(start >= 0, \"start < 0\");\n    Preconditions.checkArgument(end > start, \"end <= start\");\n\n    this.start = start;\n    this.end = end;\n    this.store = store;\n  }\n\n  /**\n   * Creates a new free entry.\n   *\n   * @param start the start of the entry\n   * @param end the end of the entry (first byte no longer in the entry)\n   * @return the entry\n   */\n  public static FileUseMapEntry<Object> makeFree(long start, long end) {\n    return new FileUseMapEntry<>(start, end, null);\n  }\n\n  /**\n   * Creates a new used entry.\n   *\n   * @param start the start of the entry\n   * @param end the end of the entry (first byte no longer in the entry)\n   * @param store the data to store in the entry\n   * @param <T> the type of data to store in the entry\n   * @return the entry\n   */\n  public static <T> FileUseMapEntry<T> makeUsed(long start, long end, T store) {\n    Preconditions.checkNotNull(store, \"store == null\");\n    return new FileUseMapEntry<>(start, end, store);\n  }\n\n  /**\n   * Obtains the first byte in the entry.\n   *\n   * @return the first byte in the entry (if the same value as {@link #getEnd()} then the entry is\n   *     empty and contains no data)\n   */\n  long getStart() {\n    return start;\n  }\n\n  /**\n   * Obtains the first byte no longer in the entry.\n   *\n   * @return the first byte no longer in the entry\n   */\n  long getEnd() {\n    return end;\n  }\n\n  /**\n   * Obtains the size of the entry.\n   *\n   * @return the number of bytes contained in the entry\n   */\n  long getSize() {\n    return end - start;\n  }\n\n  /**\n   * Determines if this is a free entry.\n   *\n   * @return is this entry free?\n   */\n  boolean isFree() {\n    return store == null;\n  }\n\n  /**\n   * Obtains the data stored in the entry.\n   *\n   * @return the data stored or {@code null} if this entry is a free entry\n   */\n  @Nullable\n  T getStore() {\n    return store;\n  }\n\n  @Override\n  public String toString() {\n    return MoreObjects.toStringHelper(this)\n        .add(\"start\", start)\n        .add(\"end\", end)\n        .add(\"store\", store)\n        .toString();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/GPFlags.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport java.io.IOException;\n\n/**\n * General purpose bit flags. Contains the encoding of the zip's general purpose bits.\n *\n * <p>We don't really care about the method bit(s). These are bits 1 and 2. Here are the values:\n *\n * <ul>\n *   <li>0 (00): Normal (-en) compression option was used.\n *   <li>1 (01): Maximum (-exx/-ex) compression option was used.\n *   <li>2 (10): Fast (-ef) compression option was used.\n *   <li>3 (11): Super Fast (-es) compression option was used.\n * </ul>\n */\nclass GPFlags {\n\n  /** Is the entry encrypted? */\n  private static final int BIT_ENCRYPTION = 1;\n\n  /** Has CRC computation been deferred and, therefore, does a data description block exist? */\n  private static final int BIT_DEFERRED_CRC = (1 << 3);\n\n  /** Is enhanced deflating used? */\n  private static final int BIT_ENHANCED_DEFLATING = (1 << 4);\n\n  /** Does the entry contain patched data? */\n  private static final int BIT_PATCHED_DATA = (1 << 5);\n\n  /** Is strong encryption used? */\n  private static final int BIT_STRONG_ENCRYPTION = (1 << 6) | (1 << 13);\n\n  /**\n   * If this bit is set the filename and comment fields for this file must be encoded using UTF-8.\n   */\n  private static final int BIT_EFS = (1 << 11);\n\n  /** Unused bits. */\n  private static final int BIT_UNUSED =\n      (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 14) | (1 << 15);\n\n  /** Bit flag value. */\n  private final long value;\n\n  /** Has the CRC computation beeen deferred? */\n  private boolean deferredCrc;\n\n  /** Is the file name encoded in UTF-8? */\n  private boolean utf8FileName;\n\n  /**\n   * Creates a new flags object.\n   *\n   * @param value the value of the bit mask\n   */\n  private GPFlags(long value) {\n    this.value = value;\n\n    deferredCrc = ((value & BIT_DEFERRED_CRC) != 0);\n    utf8FileName = ((value & BIT_EFS) != 0);\n  }\n\n  /**\n   * Obtains the flags value.\n   *\n   * @return the value of the bit mask\n   */\n  public long getValue() {\n    return value;\n  }\n\n  /**\n   * Is the CRC computation deferred?\n   *\n   * @return is the CRC computation deferred?\n   */\n  public boolean isDeferredCrc() {\n    return deferredCrc;\n  }\n\n  /**\n   * Is the file name encoded in UTF-8?\n   *\n   * @return is the file name encoded in UTF-8?\n   */\n  public boolean isUtf8FileName() {\n    return utf8FileName;\n  }\n\n  /**\n   * Creates a new bit mask.\n   *\n   * @param utf8Encoding should UTF-8 encoding be used?\n   * @return the new bit mask\n   */\n  static GPFlags make(boolean utf8Encoding) {\n    long flags = 0;\n\n    if (utf8Encoding) {\n      flags |= BIT_EFS;\n    }\n\n    return new GPFlags(flags);\n  }\n\n  /**\n   * Creates the flag information from a byte. This method will also validate that only supported\n   * options are defined in the flag.\n   *\n   * @param bits the bit mask\n   * @return the created flag information\n   * @throws IOException unsupported options are used in the bit mask\n   */\n  static GPFlags from(long bits) throws IOException {\n    if ((bits & BIT_ENCRYPTION) != 0) {\n      throw new IOException(\"Zip files with encrypted of entries not supported.\");\n    }\n\n    if ((bits & BIT_ENHANCED_DEFLATING) != 0) {\n      throw new IOException(\"Enhanced deflating not supported.\");\n    }\n\n    if ((bits & BIT_PATCHED_DATA) != 0) {\n      throw new IOException(\"Compressed patched data not supported.\");\n    }\n\n    if ((bits & BIT_STRONG_ENCRYPTION) != 0) {\n      throw new IOException(\"Strong encryption not supported.\");\n    }\n\n    if ((bits & BIT_UNUSED) != 0) {\n      throw new IOException(\n          \"Unused bits set in directory entry. Weird. I don't know what's \" + \"going on.\");\n    }\n\n    if ((bits & 0xffffffff00000000L) != 0) {\n      throw new IOException(\"Unsupported bits after 32.\");\n    }\n\n    return new GPFlags(bits);\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/InflaterByteSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.SequenceInputStream;\nimport java.util.zip.Inflater;\nimport java.util.zip.InflaterInputStream;\n\n/**\n * Byte source that inflates another byte source. It assumed the inner byte source has deflated\n * data.\n */\npublic class InflaterByteSource extends CloseableByteSource {\n\n  /** The stream factory for the deflated data. */\n  private final CloseableByteSource deflatedSource;\n\n  /**\n   * Creates a new source.\n   *\n   * @param byteSource the factory for deflated data\n   */\n  public InflaterByteSource(CloseableByteSource byteSource) {\n    deflatedSource = byteSource;\n  }\n\n  @Override\n  public InputStream openStream() throws IOException {\n    /*\n     * The extra byte is a dummy byte required by the inflater. Weirdo.\n     * (see the java.util.Inflater documentation). Looks like a hack...\n     * \"Oh, I need an extra dummy byte to allow for some... err... optimizations...\"\n     */\n    ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] {0});\n    return new InflaterInputStream(\n        new SequenceInputStream(deflatedSource.openStream(), hackByte), new Inflater(true));\n  }\n\n  @Override\n  public void innerClose() throws IOException {\n    deflatedSource.close();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/LazyDelegateByteSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.HashFunction;\nimport com.google.common.io.ByteProcessor;\nimport com.google.common.io.ByteSink;\nimport com.google.common.io.ByteSource;\nimport com.google.common.io.CharSource;\nimport com.google.common.util.concurrent.ListenableFuture;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport java.util.concurrent.ExecutionException;\n\n/**\n * {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other byte\n * source, the <em>delegate</em>, may be computed lazily.\n */\npublic class LazyDelegateByteSource extends CloseableByteSource {\n\n  /** Byte source where we delegate operations to. */\n  private final ListenableFuture<CloseableByteSource> delegate;\n\n  /**\n   * Creates a new byte source that delegates operations to the provided source.\n   *\n   * @param delegate the source that will receive all operations\n   */\n  public LazyDelegateByteSource(ListenableFuture<CloseableByteSource> delegate) {\n    this.delegate = delegate;\n  }\n\n  /**\n   * Obtains the delegate future.\n   *\n   * @return the delegate future, that may be computed or not\n   */\n  public ListenableFuture<CloseableByteSource> getDelegate() {\n    return delegate;\n  }\n\n  /**\n   * Obtains the byte source, waiting for the future to be computed.\n   *\n   * @return the byte source\n   * @throws IOException failed to compute the future :)\n   */\n  private CloseableByteSource get() throws IOException {\n    try {\n      CloseableByteSource r = delegate.get();\n      if (r == null) {\n        throw new IOException(\"Delegate byte source computation resulted in null.\");\n      }\n\n      return r;\n    } catch (InterruptedException e) {\n      throw new IOException(\"Interrupted while waiting for byte source computation.\", e);\n    } catch (ExecutionException e) {\n      throw new IOException(\"Failed to compute byte source.\", e);\n    }\n  }\n\n  @Override\n  public CharSource asCharSource(Charset charset) {\n    try {\n      return get().asCharSource(charset);\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public InputStream openBufferedStream() throws IOException {\n    return get().openBufferedStream();\n  }\n\n  @Override\n  public ByteSource slice(long offset, long length) {\n    try {\n      return get().slice(offset, length);\n    } catch (IOException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public boolean isEmpty() throws IOException {\n    return get().isEmpty();\n  }\n\n  @Override\n  public long size() throws IOException {\n    return get().size();\n  }\n\n  @Override\n  public long copyTo(OutputStream output) throws IOException {\n    return get().copyTo(output);\n  }\n\n  @Override\n  public long copyTo(ByteSink sink) throws IOException {\n    return get().copyTo(sink);\n  }\n\n  @Override\n  public byte[] read() throws IOException {\n    return get().read();\n  }\n\n  @Override\n  public <T> T read(ByteProcessor<T> processor) throws IOException {\n    return get().read(processor);\n  }\n\n  @Override\n  public HashCode hash(HashFunction hashFunction) throws IOException {\n    return get().hash(hashFunction);\n  }\n\n  @Override\n  public boolean contentEquals(ByteSource other) throws IOException {\n    return get().contentEquals(other);\n  }\n\n  @Override\n  public InputStream openStream() throws IOException {\n    return get().openStream();\n  }\n\n  @Override\n  public void innerClose() throws IOException {\n    get().close();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/NestedZip.java",
    "content": "package com.android.tools.build.apkzlib.zip;\n\nimport com.google.common.collect.Maps;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Map;\n\npublic class NestedZip extends ZFile {\n    final ZFile target;\n    final StoredEntry entry;\n    public interface NameCallback {\n        String getName(ZFile file) throws IOException;\n    }\n\n    public NestedZip(NameCallback name, ZFile target, File src, boolean mayCompress) throws IOException {\n        super(src, new ZFileOptions(), true);\n        this.target = target;\n        this.entry = target.add(name.getName(this), directOpen(0, directSize()), mayCompress);\n    }\n\n    /**\n     * @return true if lfh is consistent with cdh otherwise inconsistent\n     */\n    public boolean addFileLink(StoredEntry srcEntry, String dstName) throws IOException {\n        if (srcEntry == null)\n            throw new IOException(\"Entry \" + srcEntry + \" does not exist in nested zip\");\n        var srcName = srcEntry.getCentralDirectoryHeader().getName();\n        var offset = srcEntry.getCentralDirectoryHeader().getOffset() + srcEntry.getLocalHeaderSize();\n        if (srcName.equals(dstName)) {\n            target.addNestedLink(entry, dstName, srcEntry, srcEntry.getCentralDirectoryHeader().getOffset(), true);\n            return true;\n        } else if (offset < MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) {\n            target.addNestedLink(entry, dstName, srcEntry, offset, false);\n            return true;\n        }\n        return false;\n    }\n    public boolean addFileLink(String srcName, String dstName) throws IOException {\n        var srcEntry = get(srcName);\n        return addFileLink(srcEntry, dstName);\n    }\n\n    public ZFile getTarget() {\n        return target;\n    }\n\n    public StoredEntry getEntry() {\n        return entry;\n    }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ProcessedAndRawByteSources.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.io.Closer;\nimport java.io.Closeable;\nimport java.io.IOException;\n\n/**\n * Container that has two bytes sources: one representing raw data and another processed data. In\n * case of compression, the raw data is the compressed data and the processed data is the\n * uncompressed data. It is valid for a RaP (\"Raw-and-Processed\") to contain the same byte sources\n * for both processed and raw data.\n */\npublic class ProcessedAndRawByteSources implements Closeable {\n\n  /** The processed byte source. */\n  private final CloseableByteSource processedSource;\n\n  /** The processed raw source. */\n  private final CloseableByteSource rawSource;\n\n  /**\n   * Creates a new container.\n   *\n   * @param processedSource the processed source\n   * @param rawSource the raw source\n   */\n  public ProcessedAndRawByteSources(\n      CloseableByteSource processedSource, CloseableByteSource rawSource) {\n    this.processedSource = processedSource;\n    this.rawSource = rawSource;\n  }\n\n  /**\n   * Obtains a byte source that read the processed contents of the entry.\n   *\n   * @return a byte source\n   */\n  public CloseableByteSource getProcessedByteSource() {\n    return processedSource;\n  }\n\n  /**\n   * Obtains a byte source that reads the raw contents of an entry. This is the data that is\n   * ultimately stored in the file and, in the case of compressed files, is the same data in the\n   * source returned by {@link #getProcessedByteSource()}.\n   *\n   * @return a byte source\n   */\n  public CloseableByteSource getRawByteSource() {\n    return rawSource;\n  }\n\n  @Override\n  public void close() throws IOException {\n    Closer closer = Closer.create();\n    closer.register(processedSource);\n    closer.register(rawSource);\n    closer.close();\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntry.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.bytestorage.CloseableByteSourceFromOutputStreamBuilder;\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Supplier;\nimport com.google.common.base.Suppliers;\nimport com.google.common.base.Verify;\nimport com.google.common.io.ByteStreams;\nimport com.google.common.primitives.Ints;\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.util.Comparator;\nimport javax.annotation.Nullable;\n\n/**\n * A stored entry represents a file in the zip. The entry may or may not be written to the zip file.\n *\n * <p>Stored entries provide the operations that are related to the files themselves, not to the\n * zip. It is through the {@code StoredEntry} class that entries can be deleted ({@link #delete()},\n * open ({@link #open()}) or realigned ({@link #realign()}).\n *\n * <p>Entries are not created directly. They are created using {@link ZFile#add(String, InputStream,\n * boolean)} and obtained from the zip file using {@link ZFile#get(String)} or {@link\n * ZFile#entries()}.\n *\n * <p>Most of the data in the an entry is in the Central Directory Header. This includes the name,\n * compression method, file compressed and uncompressed sizes, CRC32 checksum, etc. The CDH can be\n * obtained using the {@link #getCentralDirectoryHeader()} method.\n */\npublic class StoredEntry {\n\n  /** Comparator that compares instances of {@link StoredEntry} by their names. */\n  static final Comparator<StoredEntry> COMPARE_BY_NAME =\n      (o1, o2) -> {\n        if (o1 == null && o2 == null) {\n          return 0;\n        }\n\n        if (o1 == null) {\n          return -1;\n        }\n\n        if (o2 == null) {\n          return 1;\n        }\n\n        String name1 = o1.getCentralDirectoryHeader().getName();\n        String name2 = o2.getCentralDirectoryHeader().getName();\n        return name1.compareTo(name2);\n      };\n\n  /** Signature of the data descriptor. */\n  private static final int DATA_DESC_SIGNATURE = 0x08074b50;\n\n  /** Local header field: signature. */\n  private static final ZipField.F4 F_LOCAL_SIGNATURE = new ZipField.F4(0, 0x04034b50, \"Signature\");\n\n  /** Local header field: version to extract, should match the CDH's. */\n  @VisibleForTesting\n  static final ZipField.F2 F_VERSION_EXTRACT =\n      new ZipField.F2(\n          F_LOCAL_SIGNATURE.endOffset(), \"Version to extract\", new ZipFieldInvariantNonNegative());\n\n  /** Local header field: GP bit flag, should match the CDH's. */\n  private static final ZipField.F2 F_GP_BIT =\n      new ZipField.F2(F_VERSION_EXTRACT.endOffset(), \"GP bit flag\");\n\n  /** Local header field: compression method, should match the CDH's. */\n  private static final ZipField.F2 F_METHOD =\n      new ZipField.F2(\n          F_GP_BIT.endOffset(), \"Compression method\", new ZipFieldInvariantNonNegative());\n\n  /** Local header field: last modification time, should match the CDH's. */\n  private static final ZipField.F2 F_LAST_MOD_TIME =\n      new ZipField.F2(F_METHOD.endOffset(), \"Last modification time\");\n\n  /** Local header field: last modification time, should match the CDH's. */\n  private static final ZipField.F2 F_LAST_MOD_DATE =\n      new ZipField.F2(F_LAST_MOD_TIME.endOffset(), \"Last modification date\");\n\n  /** Local header field: CRC32 checksum, should match the CDH's. 0 if there is no data. */\n  private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(), \"CRC32\");\n\n  /** Local header field: compressed size, size the data takes in the zip file. */\n  private static final ZipField.F4 F_COMPRESSED_SIZE =\n      new ZipField.F4(F_CRC32.endOffset(), \"Compressed size\", new ZipFieldInvariantNonNegative());\n\n  /** Local header field: uncompressed size, size the data takes after extraction. */\n  private static final ZipField.F4 F_UNCOMPRESSED_SIZE =\n      new ZipField.F4(\n          F_COMPRESSED_SIZE.endOffset(), \"Uncompressed size\", new ZipFieldInvariantNonNegative());\n\n  /** Local header field: length of the file name. */\n  private static final ZipField.F2 F_FILE_NAME_LENGTH =\n      new ZipField.F2(\n          F_UNCOMPRESSED_SIZE.endOffset(), \"@File name length\", new ZipFieldInvariantNonNegative());\n\n  /** Local header filed: length of the extra field. */\n  private static final ZipField.F2 F_EXTRA_LENGTH =\n      new ZipField.F2(\n          F_FILE_NAME_LENGTH.endOffset(), \"Extra length\", new ZipFieldInvariantNonNegative());\n\n  /** Local header size (fixed part, not counting file name or extra field). */\n  static final int FIXED_LOCAL_FILE_HEADER_SIZE = F_EXTRA_LENGTH.endOffset();\n\n  /** Type of entry. */\n  private final StoredEntryType type;\n\n  /** The central directory header with information about the file. */\n  private final CentralDirectoryHeader cdh;\n\n  /** The file this entry is associated with */\n  private final ZFile file;\n\n  /** Has this entry been deleted? */\n  private boolean deleted;\n\n  /** Extra field specified in the local directory. */\n  private ExtraField localExtra;\n\n  /** Type of data descriptor associated with the entry. */\n  private Supplier<DataDescriptorType> dataDescriptorType;\n\n  /**\n   * Source for this entry's data. If this entry is a directory, this source has to have zero size.\n   */\n  private ProcessedAndRawByteSources source;\n\n  /** Verify log for the entry. */\n  private final VerifyLog verifyLog;\n\n  /** Storage used to create buffers when loading storage into memory. */\n  private final ByteStorage storage;\n\n  /** Entry it is linking to. */\n  private final StoredEntry linkedEntry;\n\n  /** Offset of the nested link. */\n  private final long nestedOffset;\n\n  /** Dummy entry won't be written to file. */\n  private final boolean dummy;\n\n  /**\n   * Creates a new stored entry.\n   *\n   * @param header the header with the entry information; if the header does not contain an offset\n   *     it means that this entry is not yet written in the zip file\n   * @param file the zip file containing the entry\n   * @param source the entry's data source; it can be {@code null} only if the source can be read\n   *     from the zip file, that is, if {@code header.getOffset()} is non-negative\n   * @throws IOException failed to create the entry\n   */\n  StoredEntry(\n          CentralDirectoryHeader header,\n          ZFile file,\n          @Nullable ProcessedAndRawByteSources source,\n          ByteStorage storage)\n          throws IOException {\n      this(header, file, source, storage, null, 0, false);\n  }\n\n  StoredEntry(\n          String name,\n          ZFile file,\n          ByteStorage storage,\n          StoredEntry linkedEntry,\n          StoredEntry nestedEntry,\n          long nestedOffset,\n          boolean dummy)\n          throws IOException {\n      this((nestedEntry == null ? linkedEntry: nestedEntry).linkingCentralDirectoryHeader(name, file),\n              file, (nestedEntry == null ? linkedEntry : nestedEntry).getSource(), storage, linkedEntry, nestedOffset, dummy);\n  }\n\n  private CentralDirectoryHeader linkingCentralDirectoryHeader(String name, ZFile file) {\n    boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name);\n    GPFlags flags = GPFlags.make(encodeWithUtf8);\n    return cdh.link(name, EncodeUtils.encode(name, flags), flags, file);\n  }\n\n  private StoredEntry(\n      CentralDirectoryHeader header,\n      ZFile file,\n      @Nullable ProcessedAndRawByteSources source,\n      ByteStorage storage,\n      StoredEntry linkedEntry,\n      long nestedOffset,\n      boolean dummy)\n      throws IOException {\n    cdh = header;\n    this.file = file;\n    deleted = false;\n    verifyLog = file.makeVerifyLog();\n    this.storage = storage;\n    this.linkedEntry = linkedEntry;\n    this.nestedOffset = nestedOffset;\n    this.dummy = dummy;\n\n    if (header.getOffset() >= 0) {\n      readLocalHeader();\n\n      Preconditions.checkArgument(\n          source == null, \"Source was defined but contents already exist on file.\");\n\n      /*\n       * Since the file is already in the zip, dynamically create a source that will read\n       * the file from the zip when needed. The assignment is not really needed, but we\n       * would get a warning because of the @NotNull otherwise.\n       */\n      this.source = createSourceFromZip(cdh.getOffset());\n    } else {\n      /*\n       * There is no local extra data for new files.\n       */\n      localExtra = new ExtraField();\n\n      Preconditions.checkNotNull(source, \"Source was not defined, but contents are not on file.\");\n      this.source = source;\n    }\n\n    /*\n     * It seems that zip utilities store directories as names ending with \"/\".\n     * This seems to be respected by all zip utilities although I could not find there anywhere\n     * in the specification.\n     */\n    if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {\n      type = StoredEntryType.DIRECTORY;\n      verifyLog.verify(\n          this.source.getProcessedByteSource().isEmpty(), \"Directory source is not empty.\");\n      verifyLog.verify(cdh.getCrc32() == 0, \"Directory has CRC32 = %s.\", cdh.getCrc32());\n      verifyLog.verify(\n          cdh.getUncompressedSize() == 0,\n          \"Directory has uncompressed size = %s.\",\n          cdh.getUncompressedSize());\n\n      /*\n       * Some clever (OMG!) tools, like jar will actually try to compress the directory\n       * contents and generate a 2 byte compressed data. Of course, the uncompressed size is\n       * zero and we're just wasting space.\n       */\n      long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize();\n      verifyLog.verify(\n          compressedSize == 0 || compressedSize == 2,\n          \"Directory has compressed size = %s.\",\n          compressedSize);\n    } else {\n      type = StoredEntryType.FILE;\n    }\n\n    /*\n     * By default we assume there is no data descriptor unless the CRC is marked as deferred\n     * in the header's GP Bit.\n     */\n    dataDescriptorType = Suppliers.ofInstance(DataDescriptorType.NO_DATA_DESCRIPTOR);\n    if (header.getGpBit().isDeferredCrc()) {\n      /*\n       * If the deferred CRC bit exists, then we have an extra descriptor field. This extra\n       * field may have a signature.\n       */\n      Verify.verify(\n          header.getOffset() >= 0,\n          \"Files that are not on disk cannot have the \" + \"deferred CRC bit set.\");\n\n      dataDescriptorType =\n          Suppliers.memoize(\n              () -> {\n                try {\n                  return readDataDescriptorRecord();\n                } catch (IOException e) {\n                  throw new IOExceptionWrapper(\n                      new IOException(\"Failed to read data descriptor record.\", e));\n                }\n              });\n    }\n  }\n\n  /**\n   * Obtains the size of the local header of this entry.\n   *\n   * @return the local header size in bytes\n   */\n  public int getLocalHeaderSize() {\n    Preconditions.checkState(!deleted, \"deleted\");\n    return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size();\n  }\n\n  /**\n   * Obtains the size of the whole entry on disk, including local header and data descriptor. This\n   * method will wait until compression information is complete, if needed.\n   *\n   * @return the number of bytes\n   * @throws IOException failed to get compression information\n   */\n  long getInFileSize() throws IOException {\n    Preconditions.checkState(!deleted, \"deleted\");\n    return cdh.getCompressionInfoWithWait().getCompressedSize()\n        + getLocalHeaderSize()\n        + dataDescriptorType.get().size;\n  }\n\n  /**\n   * Obtains a stream that allows reading from the entry.\n   *\n   * @return a stream that will return as many bytes as the uncompressed entry size\n   * @throws IOException failed to open the stream\n   */\n  public InputStream open() throws IOException {\n    return source.getProcessedByteSource().openStream();\n  }\n\n  /**\n   * Obtains the contents of the file.\n   *\n   * @return a byte array with the contents of the file (uncompressed if the file was compressed)\n   * @throws IOException failed to read the file\n   */\n  public byte[] read() throws IOException {\n    try (InputStream is = new BufferedInputStream(open())) {\n      return ByteStreams.toByteArray(is);\n    }\n  }\n\n  /**\n   * Obtains the contents of the file in an existing buffer.\n   *\n   * @param bytes buffer to read the file contents in.\n   * @return the number of bytes read\n   * @throws IOException failed to read the file.\n   */\n  public int read(byte[] bytes) throws IOException {\n    if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) {\n      throw new RuntimeException(\n          \"Buffer to small while reading {}\" + getCentralDirectoryHeader().getName());\n    }\n    try (InputStream is = new BufferedInputStream(open())) {\n      return ByteStreams.read(is, bytes, 0, bytes.length);\n    }\n  }\n\n  /**\n   * Obtains the type of entry.\n   *\n   * @return the type of entry\n   */\n  public StoredEntryType getType() {\n    Preconditions.checkState(!deleted, \"deleted\");\n    return type;\n  }\n\n  /**\n   * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. To\n   * eventually write updates to disk, {@link ZFile#update()} must be called.\n   *\n   * @throws IOException failed to delete the entry\n   * @throws IllegalStateException if the zip file was open in read-only mode\n   */\n  public void delete() throws IOException {\n    delete(true);\n  }\n\n  /**\n   * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself. To\n   * eventually write updates to disk, {@link ZFile#update()} must be called.\n   *\n   * @param notify should listeners be notified of the deletion? This will only be {@code false} if\n   *     the entry is being removed as part of a replacement\n   * @throws IOException failed to delete the entry\n   * @throws IllegalStateException if the zip file was open in read-only mode\n   */\n  void delete(boolean notify) throws IOException {\n    Preconditions.checkState(!deleted, \"deleted\");\n    file.delete(this, notify);\n    deleted = true;\n    source.close();\n  }\n\n  /** Returns {@code true} if this entry has been deleted/replaced. */\n  public boolean isDeleted() {\n    return deleted;\n  }\n\n  /**\n   * Obtains the CDH associated with this entry.\n   *\n   * @return the CDH\n   */\n  public CentralDirectoryHeader getCentralDirectoryHeader() {\n    return cdh;\n  }\n\n  /**\n   * Reads the file's local header and verifies that it matches the Central Directory Header\n   * provided in the constructor. This method should only be called if the entry already exists on\n   * disk; new entries do not have local headers.\n   *\n   * <p>This method will define the {@link #localExtra} field that is only defined in the local\n   * descriptor.\n   *\n   * @throws IOException failed to read the local header\n   */\n  private void readLocalHeader() throws IOException {\n    byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE];\n    file.directFullyRead(cdh.getOffset(), localHeader);\n\n    CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();\n\n    ByteBuffer bytes = ByteBuffer.wrap(localHeader);\n    F_LOCAL_SIGNATURE.verify(bytes);\n    F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog);\n    F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog);\n    F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog);\n\n    if (file.areTimestampsIgnored()) {\n      F_LAST_MOD_TIME.skip(bytes);\n      F_LAST_MOD_DATE.skip(bytes);\n    } else {\n      F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog);\n      F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog);\n    }\n\n    /*\n     * If CRC-32, compressed size and uncompressed size are deferred, their values in Local\n     * File Header must be ignored and their actual values must be read from the Data\n     * Descriptor following the contents of this entry. See readDataDescriptorRecord().\n     */\n    if (cdh.getGpBit().isDeferredCrc()) {\n      F_CRC32.skip(bytes);\n      F_COMPRESSED_SIZE.skip(bytes);\n      F_UNCOMPRESSED_SIZE.skip(bytes);\n    } else {\n      F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog);\n      F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog);\n      F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog);\n    }\n\n    F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length);\n    long extraLength = F_EXTRA_LENGTH.read(bytes);\n    long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset();\n    byte[] fileNameData = new byte[cdh.getEncodedFileName().length];\n    file.directFullyRead(fileNameStart, fileNameData);\n\n    String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit());\n    if (!fileName.equals(cdh.getName())) {\n      verifyLog.log(\n          String.format(\n              \"Central directory reports file as being named '%s' but local header\"\n                  + \"reports file being named '%s'.\",\n              cdh.getName(), fileName));\n    }\n\n    long localExtraStart = fileNameStart + cdh.getEncodedFileName().length;\n    byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)];\n    file.directFullyRead(localExtraStart, localExtraRaw);\n    localExtra = new ExtraField(localExtraRaw);\n  }\n\n  /**\n   * Reads the data descriptor record. This method can only be invoked once it is established that a\n   * data descriptor does exist. It will read the data descriptor and check that the data described\n   * there matches the data provided in the Central Directory.\n   *\n   * <p>This method will set the {@link #dataDescriptorType} field to the appropriate type of data\n   * descriptor record.\n   *\n   * @throws IOException failed to read the data descriptor record\n   */\n  private DataDescriptorType readDataDescriptorRecord() throws IOException {\n    CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();\n\n    long ddStart =\n        cdh.getOffset()\n            + FIXED_LOCAL_FILE_HEADER_SIZE\n            + cdh.getName().length()\n            + localExtra.size()\n            + compressInfo.getCompressedSize();\n    byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];\n    file.directFullyRead(ddStart, ddData);\n\n    ByteBuffer ddBytes = ByteBuffer.wrap(ddData);\n\n    ZipField.F4 signatureField = new ZipField.F4(0, \"Data descriptor signature\");\n    int cpos = ddBytes.position();\n    long sig = signatureField.read(ddBytes);\n    DataDescriptorType result;\n    if (sig == DATA_DESC_SIGNATURE) {\n      result = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;\n    } else {\n      result = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;\n      ddBytes.position(cpos);\n    }\n\n    ZipField.F4 crc32Field = new ZipField.F4(0, \"CRC32\");\n    ZipField.F4 compressedField = new ZipField.F4(crc32Field.endOffset(), \"Compressed size\");\n    ZipField.F4 uncompressedField =\n        new ZipField.F4(compressedField.endOffset(), \"Uncompressed size\");\n\n    crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog);\n    compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog);\n    uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog);\n    return result;\n  }\n\n  /**\n   * Creates a new source that reads data from the zip.\n   *\n   * @param zipOffset the offset into the zip file where the data is, must be non-negative\n   * @throws IOException failed to close the old source\n   * @return the created source\n   */\n  private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset) throws IOException {\n    Preconditions.checkArgument(zipOffset >= 0, \"zipOffset < 0\");\n\n    final CentralDirectoryHeaderCompressInfo compressInfo;\n    try {\n      compressInfo = cdh.getCompressionInfoWithWait();\n    } catch (IOException e) {\n      throw new RuntimeException(\n          \"IOException should never occur here because compression \"\n              + \"information should be immediately available if reading from zip.\",\n          e);\n    }\n\n    /*\n     * Create a source that will return whatever is on the zip file.\n     */\n    CloseableByteSource rawContents =\n        new CloseableByteSource() {\n          @Override\n          public long size() throws IOException {\n            return compressInfo.getCompressedSize();\n          }\n\n          @Override\n          public InputStream openStream() throws IOException {\n            Preconditions.checkState(!deleted, \"deleted\");\n\n            long dataStart = zipOffset + getLocalHeaderSize();\n            long dataEnd = dataStart + compressInfo.getCompressedSize();\n\n            file.openReadOnlyIfClosed();\n            return file.directOpen(dataStart, dataEnd);\n          }\n\n          @Override\n          protected void innerClose() throws IOException {\n            /*\n             * Nothing to do here.\n             */\n          }\n        };\n\n    return createSourcesFromRawContents(rawContents);\n  }\n\n  /**\n   * Creates a {@link ProcessedAndRawByteSources} from the raw data source . The processed source\n   * will either inflate or do nothing depending on the compression information that, at this point,\n   * should already be available\n   *\n   * @param rawContents the raw data to create the source from\n   * @return the sources for this entry\n   */\n  private ProcessedAndRawByteSources createSourcesFromRawContents(CloseableByteSource rawContents) {\n    CentralDirectoryHeaderCompressInfo compressInfo;\n    try {\n      compressInfo = cdh.getCompressionInfoWithWait();\n    } catch (IOException e) {\n      throw new RuntimeException(\n          \"IOException should never occur here because compression \"\n              + \"information should be immediately available if creating from raw \"\n              + \"contents.\",\n          e);\n    }\n\n    CloseableByteSource contents;\n\n    /*\n     * If the contents are deflated, wrap that source in an inflater source so we get the\n     * uncompressed data.\n     */\n    if (compressInfo.getMethod() == CompressionMethod.DEFLATE) {\n      contents = new InflaterByteSource(rawContents);\n    } else {\n      contents = rawContents;\n    }\n\n    return new ProcessedAndRawByteSources(contents, rawContents);\n  }\n\n  /**\n   * Replaces {@link #source} with one that reads file data from the zip file.\n   *\n   * @param zipFileOffset the offset in the zip file where data is written; must be non-negative\n   * @throws IOException failed to replace the source\n   */\n  void replaceSourceFromZip(long zipFileOffset) throws IOException {\n    Preconditions.checkArgument(zipFileOffset >= 0, \"zipFileOffset < 0\");\n\n    ProcessedAndRawByteSources oldSource = source;\n    source = createSourceFromZip(zipFileOffset);\n    cdh.setOffset(zipFileOffset);\n    if (!isLinkingEntry())\n      oldSource.close();\n  }\n\n  /**\n   * Loads all data in memory and replaces {@link #source} with one that contains all the data in\n   * memory.\n   *\n   * <p>If the entry's contents are already in memory, this call does nothing.\n   *\n   * @throws IOException failed to replace the source\n   */\n  void loadSourceIntoMemory() throws IOException {\n    if (cdh.getOffset() == -1) {\n      /*\n       * No offset in the CDR means data has not been written to disk which, in turn,\n       * means data is already loaded into memory.\n       */\n      return;\n    }\n\n    CloseableByteSourceFromOutputStreamBuilder rawBuilder = storage.makeBuilder();\n    try (InputStream input = source.getRawByteSource().openStream()) {\n      ByteStreams.copy(input, rawBuilder);\n    }\n\n    CloseableByteSource newRaw = rawBuilder.build();\n    ProcessedAndRawByteSources newSources = createSourcesFromRawContents(newRaw);\n\n    try (ProcessedAndRawByteSources oldSource = source) {\n      source = newSources;\n      cdh.setOffset(-1);\n    }\n  }\n\n  /**\n   * Obtains the source data for this entry. This method can only be called for files, it cannot be\n   * called for directories.\n   *\n   * @return the entry source\n   */\n  ProcessedAndRawByteSources getSource() {\n    return source;\n  }\n\n  /**\n   * Obtains the type of data descriptor used in the entry.\n   *\n   * @return the type of data descriptor\n   */\n  public DataDescriptorType getDataDescriptorType() {\n    return dataDescriptorType.get();\n  }\n\n  /**\n   * Removes the data descriptor, if it has one and resets the data descriptor bit in the central\n   * directory header.\n   *\n   * @return was the data descriptor remove?\n   */\n  boolean removeDataDescriptor() {\n    if (dataDescriptorType.get() == DataDescriptorType.NO_DATA_DESCRIPTOR) {\n      return false;\n    }\n\n    dataDescriptorType = Suppliers.ofInstance(DataDescriptorType.NO_DATA_DESCRIPTOR);\n    cdh.resetDeferredCrc();\n    return true;\n  }\n\n  /**\n   * Obtains the local header data.\n   *\n   * @param buffer a buffer to write header data to\n   * @return the header data size\n   * @throws IOException failed to get header byte data\n   */\n  int toHeaderData(byte[] buffer) throws IOException {\n    Preconditions.checkArgument(\n        buffer.length\n            >= F_EXTRA_LENGTH.endOffset() + cdh.getEncodedFileName().length + localExtra.size(),\n        \"Buffer should be at least the header size\");\n\n    ByteBuffer out = ByteBuffer.wrap(buffer);\n    writeData(out);\n    return out.position();\n  }\n\n  private void writeData(ByteBuffer out) throws IOException {\n    writeData(out, 0);\n  }\n\n  void writeData(ByteBuffer out, int extraOffset) throws IOException {\n    Preconditions.checkArgument(extraOffset >= 0 , \"extraOffset < 0\");\n    CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();\n\n    F_LOCAL_SIGNATURE.write(out);\n    F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract());\n    F_GP_BIT.write(out, cdh.getGpBit().getValue());\n    F_METHOD.write(out, compressInfo.getMethod().methodCode);\n\n    if (file.areTimestampsIgnored()) {\n      F_LAST_MOD_TIME.write(out, 0);\n      F_LAST_MOD_DATE.write(out, 0);\n    } else {\n      F_LAST_MOD_TIME.write(out, cdh.getLastModTime());\n      F_LAST_MOD_DATE.write(out, cdh.getLastModDate());\n    }\n\n    F_CRC32.write(out, cdh.getCrc32());\n    F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize());\n    F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize());\n    F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length);\n    F_EXTRA_LENGTH.write(out, localExtra.size() + extraOffset + nestedOffset);\n\n    out.put(cdh.getEncodedFileName());\n    localExtra.write(out);\n  }\n\n  /**\n   * Requests that this entry be realigned. If this entry is already aligned according to the rules\n   * in {@link ZFile} then this method does nothing. Otherwise it will move the file's data into\n   * memory and place it in a different area of the zip.\n   *\n   * @return has this file been changed? Note that if the entry has not yet been written on the\n   *     file, realignment does not count as a change as nothing needs to be updated in the file;\n   *     also, if the entry has been changed, this object may have been marked as deleted and a new\n   *     stored entry may need to be fetched from the file\n   * @throws IOException failed to realign the entry; the entry may no longer exist in the zip file\n   */\n  public boolean realign() throws IOException {\n    Preconditions.checkState(!deleted, \"Entry has been deleted.\");\n\n    if (isLinkingEntry()) return true;\n\n    return file.realign(this);\n  }\n\n  public boolean isLinkingEntry() {\n    return linkedEntry != null;\n  }\n\n  public boolean isDummyEntry() {\n    return dummy;\n  }\n\n  public long getNestedOffset() {\n    return nestedOffset;\n  }\n\n  /**\n   * Obtains the contents of the local extra field.\n   *\n   * @return the contents of the local extra field\n   */\n  public ExtraField getLocalExtra() {\n    return localExtra;\n  }\n\n  /**\n   * Sets the contents of the local extra field.\n   *\n   * @param localExtra the contents of the local extra field\n   * @throws IOException failed to update the zip file\n   */\n  public void setLocalExtra(ExtraField localExtra) throws IOException {\n    boolean resized = setLocalExtraNoNotify(localExtra);\n    file.localHeaderChanged(this, resized);\n  }\n\n  /**\n   * Sets the contents of the local extra field, does not notify the {@link ZFile} of the change.\n   * This is used internally when the {@link ZFile} itself wants to change the local extra and\n   * doesn't need the callback.\n   *\n   * @param localExtra the contents of the local extra field\n   * @return has the local header size changed?\n   * @throws IOException failed to load the file\n   */\n  boolean setLocalExtraNoNotify(ExtraField localExtra) throws IOException {\n    boolean sizeChanged;\n\n    /*\n     * Make sure we load into memory.\n     *\n     * If we change the size of the local header, the actual start of the file changes\n     * according to our in-memory structures so, if we don't read the file now, we won't be\n     * able to load it later :)\n     *\n     * But, even if the size doesn't change, we need to read it force the entry to be\n     * rewritten otherwise the changes in the local header aren't written. Of course this case\n     * may be optimized with some extra complexity added :)\n     */\n    loadSourceIntoMemory();\n\n    if (this.localExtra.size() != localExtra.size()) {\n      sizeChanged = true;\n    } else {\n      sizeChanged = false;\n    }\n\n    this.localExtra = localExtra;\n    return sizeChanged;\n  }\n\n  /**\n   * Obtains the verify log for the entry.\n   *\n   * @return the verify log\n   */\n  public VerifyLog getVerifyLog() {\n    return verifyLog;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/StoredEntryType.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/** Type of stored entry. */\npublic enum StoredEntryType {\n  /** Entry is a file. */\n  FILE,\n\n  /** Entry is a directory. */\n  DIRECTORY\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLog.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.google.common.collect.ImmutableList;\n\n/**\n * The verify log contains verification messages. It is used to capture validation issues with a zip\n * file or with parts of a zip file.\n */\npublic interface VerifyLog {\n\n  /**\n   * Logs a message.\n   *\n   * @param message the message to verify\n   */\n  void log(String message);\n\n  /**\n   * Obtains all save logged messages.\n   *\n   * @return the logged messages\n   */\n  ImmutableList<String> getLogs();\n\n  /**\n   * Performs verification of a non-critical condition, logging a message if the condition is not\n   * verified.\n   *\n   * @param condition the condition\n   * @param message the message to write if {@code condition} is {@code false}.\n   * @param args arguments for formatting {@code message} using {@code String.format}\n   */\n  default void verify(boolean condition, String message, Object... args) {\n    if (!condition) {\n      log(String.format(message, args));\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/VerifyLogs.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.google.common.collect.ImmutableList;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/** Factory for verification logs. */\nfinal class VerifyLogs {\n\n  private VerifyLogs() {}\n\n  /**\n   * Creates a {@link VerifyLog} that ignores all messages logged.\n   *\n   * @return the log\n   */\n  static VerifyLog devNull() {\n    return new VerifyLog() {\n      @Override\n      public void log(String message) {}\n\n      @Override\n      public ImmutableList<String> getLogs() {\n        return ImmutableList.of();\n      }\n    };\n  }\n\n  /**\n   * Creates a {@link VerifyLog} that stores all log messages.\n   *\n   * @return the log\n   */\n  static VerifyLog unlimited() {\n    return new VerifyLog() {\n\n      /** All saved messages. */\n      private final List<String> messages = new ArrayList<>();\n\n      @Override\n      public void log(String message) {\n        messages.add(message);\n      }\n\n      @Override\n      public ImmutableList<String> getLogs() {\n        return ImmutableList.copyOf(messages);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZFile.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.apksig.util.DataSource;\nimport com.android.apksig.util.DataSources;\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.utils.CachedFileContents;\nimport com.android.tools.build.apkzlib.utils.IOExceptionFunction;\nimport com.android.tools.build.apkzlib.utils.IOExceptionRunnable;\nimport com.android.tools.build.apkzlib.zip.compress.Zip64NotSupportedException;\nimport com.android.tools.build.apkzlib.zip.utils.ByteTracker;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;\nimport com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;\nimport com.google.common.base.Objects;\nimport com.google.common.base.Optional;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Predicate;\nimport com.google.common.base.Supplier;\nimport com.google.common.base.Verify;\nimport com.google.common.base.VerifyException;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport com.google.common.hash.Hashing;\nimport com.google.common.io.ByteSource;\nimport com.google.common.io.Closer;\nimport com.google.common.primitives.Ints;\nimport com.google.common.util.concurrent.FutureCallback;\nimport com.google.common.util.concurrent.Futures;\nimport com.google.common.util.concurrent.ListenableFuture;\nimport com.google.common.util.concurrent.MoreExecutors;\nimport com.google.common.util.concurrent.SettableFuture;\nimport java.io.ByteArrayInputStream;\nimport java.io.Closeable;\nimport java.io.EOFException;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.RandomAccessFile;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.SortedSet;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Future;\nimport javax.annotation.Nullable;\n\n/**\n * The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile} can\n * be created on a new file or in an existing file. Once created, files can be added or removed from\n * the zip file.\n *\n * <p>Changes in the zip file are always deferred. Any change requested is made in memory and\n * written to disk only when {@link #update()} or {@link #close()} is invoked.\n *\n * <p>Zip files are open initially in read-only mode and will switch to read-write when needed. This\n * is done automatically. Because modifications to the file are done in-memory, the zip file can be\n * manipulated when closed. When invoking {@link #update()} or {@link #close()} the zip file will be\n * reopen and changes will be written. However, the zip file cannot be modified outside the control\n * of {@code ZFile}. So, if a {@code ZFile} is closed, modified outside and then a file is added or\n * removed from the zip file, when reopening the zip file, {@link ZFile} will detect the outside\n * modification and will fail.\n *\n * <p>In memory manipulation means that files added to the zip file are kept in memory until written\n * to disk. This provides much faster operation and allows better zip file allocation (see below).\n * It may, however, increase the memory footprint of the application. When adding large files, if\n * memory consumption is a concern, a call to {@link #update()} will actually write the file to disk\n * and discard the memory buffer. Information about allocation can be obtained from a {@link\n * ByteTracker} that can be given to the file on creation.\n *\n * <p>{@code ZFile} keeps track of allocation inside of the zip file. If a file is deleted, its\n * space is marked as freed and will be reused for an added file if it fits in the space. Allocation\n * of files to empty areas is done using a <em>best fit</em> algorithm. When adding a file, if it\n * doesn't fit in any free area, the zip file will be extended.\n *\n * <p>{@code ZFile} provides a fast way to merge data from another zip file (see {@link\n * #mergeFrom(ZFile, Predicate)}) avoiding recompression and copying of equal files. When merging,\n * patterns of files may be provided that are ignored. This allows handling special files in the\n * merging process, such as files in {@code META-INF}.\n *\n * <p>When adding files to the zip file, unless files are explicitly required to be stored, files\n * will be deflated. However, deflating will not occur if the deflated file is larger then the\n * stored file, <em>e.g.</em> if compression would yield a bigger file. See {@link Compressor} for\n * details on how compression works.\n *\n * <p>Because {@code ZFile} was designed to be used in a build system and not as general-purpose zip\n * utility, it is very strict (and unforgiving) about the zip format and unsupported features.\n *\n * <p>{@code ZFile} supports <em>alignment</em>. Alignment means that file data (not entries -- the\n * local header must be discounted) must start at offsets that are multiple of a number -- the\n * alignment. Alignment is defined by an alignment rules ({@link AlignmentRule} in the {@link\n * ZFileOptions} object used to create the {@link ZFile}.\n *\n * <p>When a file is added to the zip, the alignment rules will be checked and alignment will be\n * honored when positioning the file in the zip. This means that unused spaces in the zip may be\n * generated as a result. However, alignment of existing entries will not be changed.\n *\n * <p>Entries can be realigned individually (see {@link StoredEntry#realign()} or the full zip file\n * may be realigned (see {@link #realign()}). When realigning the full zip entries that are already\n * aligned will not be affected.\n *\n * <p>Because realignment may cause files to move in the zip, realignment is done in-memory meaning\n * that files that need to change location will moved to memory and will only be flushed when either\n * {@link #update()} or {@link #close()} are called.\n *\n * <p>Alignment only applies to filed that are forced to be uncompressed. This is because alignment\n * is used to allow mapping files in the archive directly into memory and compressing defeats the\n * purpose of alignment.\n *\n * <p>Manipulating zip files with {@link ZFile} may yield zip files with empty spaces between files.\n * This happens in two situations: (1) if alignment is required, files may be shifted to conform to\n * the request alignment leaving an empty space before the previous file, and (2) if a file is\n * removed or replaced with a file that does not fit the space it was in. By default, {@link ZFile}\n * does not do any special processing in these situations. Files are indexed by their offsets from\n * the central directory and empty spaces can exist in the zip file.\n *\n * <p>However, it is possible to tell {@link ZFile} to use the extra field in the local header to do\n * cover the empty spaces. This is done by setting {@link\n * ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} to {@code true}. This has the advantage\n * of leaving no gaps between entries in the zip, as required by some tools like Oracle's {code jar}\n * tool. However, setting this option will destroy the contents of the file's extra field.\n *\n * <p>Activating {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} may lead to\n * <i>virtual files</i> being added to the zip file. Since extra field is limited to 64k, it is not\n * possible to cover any space bigger than that using the extra field. In those cases, <i>virtual\n * files</i> are added to the file. A virtual file is a file that exists in the actual zip data, but\n * is not referenced from the central directory. A zip-compliant utility should ignore these files.\n * However, zip utilities that expect the zip to be a stream, such as Oracle's jar, will find these\n * files instead of considering the zip to be corrupt.\n *\n * <p>{@code ZFile} support sorting zip files. Sorting (done through the {@link #sortZipContents()}\n * method) is a process by which all files are re-read into memory, if not already in memory,\n * removed from the zip and re-added in alphabetical order, respecting alignment rules. So, in\n * general, file {@code b} will come after file {@code a} unless file {@code a} is subject to\n * alignment that forces an empty space before that can be occupied by {@code b}. Sorting can be\n * used to minimize the changes between two zips.\n *\n * <p>Sorting in {@code ZFile} can be done manually or automatically. Manual sorting is done by\n * invoking {@link #sortZipContents()}. Automatic sorting is done by setting the {@link\n * ZFileOptions#getAutoSortFiles()} option when creating the {@code ZFile}. Automatic sorting\n * invokes {@link #sortZipContents()} immediately when doing an {@link #update()} after all\n * extensions have processed the {@link ZFileExtension#beforeUpdate()}. This has the guarantee that\n * files added by extensions will be sorted, something that does not happen if the invocation is\n * sequential, <i>i.e.</i>, {@link #sortZipContents()} called before {@link #update()}. The drawback\n * of automatic sorting is that sorting will happen every time {@link #update()} is called and the\n * file is dirty having a possible penalty in performance.\n *\n * <p>To allow whole-apk signing, the {@code ZFile} allows the central directory location to be\n * offset by a fixed amount. This amount can be set using the {@link #setExtraDirectoryOffset(long)}\n * method. Setting a non-zero value will add extra (unused) space in the zip file before the central\n * directory. This value can be changed at any time and it will force the central directory\n * rewritten when the file is updated or closed.\n *\n * <p>{@code ZFile} provides an extension mechanism to allow objects to register with the file and\n * be notified when changes to the file happen. This should be used to add extra features to the zip\n * file while providing strong decoupling. See {@link ZFileExtension}, {@link\n * ZFile#addZFileExtension(ZFileExtension)} and {@link ZFile#removeZFileExtension(ZFileExtension)}.\n *\n * <p>This class is <strong>not</strong> thread-safe. Neither are any of the classes associated with\n * it in this package, except when otherwise noticed.\n */\npublic class ZFile implements Closeable {\n\n  /**\n   * The file separator in paths in the zip file. This is fixed by the zip specification (section\n   * 4.4.17).\n   */\n  public static final char SEPARATOR = '/';\n\n  /** Minimum size the EOCD can have. */\n  private static final int MIN_EOCD_SIZE = 22;\n\n  /** Number of bytes of the Zip64 EOCD locator record. */\n  private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;\n\n  /** Maximum size for the EOCD. */\n  private static final int MAX_EOCD_COMMENT_SIZE = 65535;\n\n  /** How many bytes to look back from the end of the file to look for the EOCD signature. */\n  private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE;\n\n  /** Signature of the Zip64 EOCD locator record. */\n  private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;\n\n  /** Signature of the EOCD record. */\n  private static final byte[] EOCD_SIGNATURE = new byte[] {0x06, 0x05, 0x4b, 0x50};\n\n  /** Size of buffer for I/O operations. */\n  private static final int IO_BUFFER_SIZE = 1024 * 1024;\n\n  /**\n   * When extensions request re-runs, we do maximum number of cycles until we decide to stop and\n   * flag a infinite recursion problem.\n   */\n  private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10;\n\n  /**\n   * Minimum size for the extra field when we have to add one. We rely on the alignment segment to\n   * do that so the minimum size for the extra field is the minimum size of an alignment segment.\n   */\n  protected static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE;\n\n  /**\n   * Maximum size of the extra field.\n   *\n   * <p>Theoretically, this is (1 << 16) - 1 = 65535 and not (1 < 15) -1 = 32767. However, due to\n   * http://b.android.com/221703, we need to keep this limited.\n   */\n  protected static final int MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE = (1 << 15) - 1;\n\n  /** File zip file. */\n  protected final File file;\n\n  /**\n   * The random access file used to access the zip file. This will be {@code null} if and only if\n   * {@link #state} is {@link ZipFileState#CLOSED}.\n   */\n  @Nullable private RandomAccessFile raf;\n\n  /**\n   * The map containing the in-memory contents of the zip file. It keeps track of which parts of the\n   * zip file are used and which are not.\n   */\n  private final FileUseMap map;\n\n  /**\n   * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the one\n   * that exists on disk is no longer valid (because the zip has been changed).\n   *\n   * <p>If the EOCD is deleted because the zip has been changed and the old EOCD was no longer\n   * valid, then {@link #eocdComment} will contain the comment saved from the EOCD.\n   */\n  @Nullable private FileUseMapEntry<Eocd> eocdEntry;\n\n  /**\n   * The Central Directory entry. Will be {@code null} if there is no Central Directory (because the\n   * zip is new) or because the one that exists on disk is no longer valid (because the zip has been\n   * changed).\n   */\n  @Nullable private FileUseMapEntry<CentralDirectory> directoryEntry;\n\n  /**\n   * All entries in the zip file. It includes in-memory changes and may not reflect what is written\n   * on disk. Only entries that have been compressed are in this list.\n   */\n  private final Map<String, FileUseMapEntry<StoredEntry>> entries;\n\n  /**\n   * Entries added to the zip file, but that are not yet compressed. When compression is done, these\n   * entries are eventually moved to {@link #entries}. uncompressedEntries is a list because entries\n   * need to be kept in the order by which they were added. It allows adding multiple files with the\n   * same name and getting the right notifications on which files replaced which.\n   *\n   * <p>Files are placed in this list in {@link #add(StoredEntry)} method. This method will keep\n   * files here temporarily and move then to {@link #entries} when the data is available.\n   *\n   * <p>Moving files out of this list to {@link #entries} is done by {@link\n   * #processAllReadyEntries()}.\n   */\n  private final List<StoredEntry> uncompressedEntries;\n\n  /** LSPatch\n   *\n   */\n  private final List<StoredEntry> linkingEntries;\n\n  /** Current state of the zip file. */\n  private ZipFileState state;\n\n  /**\n   * Are the in-memory changes that have not been written to the zip file?\n   *\n   * <p>This might be false, but will become true after {@link #processAllReadyEntriesWithWait()} is\n   * called if there are {@link #uncompressedEntries} compressing in the background.\n   */\n  private boolean dirty;\n\n  /**\n   * Non-{@code null} only if the file is currently closed. Used to detect if the zip is modified\n   * outside this object's control. If the file has never been written, this will be {@code null}\n   * even if it is closed.\n   */\n  @Nullable private CachedFileContents<Object> closedControl;\n\n  /** The alignment rule. */\n  private final AlignmentRule alignmentRule;\n\n  /** Extensions registered with the file. */\n  private final List<ZFileExtension> extensions;\n\n  /**\n   * When notifying extensions, extensions may request that some runnables are executed. This list\n   * collects all runnables by the order they were requested. Together with {@link #isNotifying}, it\n   * is used to avoid reordering notifications.\n   */\n  private final List<IOExceptionRunnable> toRun;\n\n  /**\n   * {@code true} when {@link #notify(com.android.tools.build.apkzlib.utils.IOExceptionFunction)} is\n   * notifying extensions. Used to avoid reordering notifications.\n   */\n  private boolean isNotifying;\n\n  /**\n   * An extra offset for the central directory location. {@code 0} if the central directory should\n   * be written in its standard location.\n   */\n  private long extraDirectoryOffset;\n\n  /** Should all timestamps be zeroed when reading / writing the zip? */\n  private boolean noTimestamps;\n\n  /** Compressor to use. */\n  private final Compressor compressor;\n\n  /** Byte storage to use. */\n  private final ByteStorage storage;\n\n  /** Use the zip entry's \"extra field\" field to cover empty space in the zip file? */\n  private boolean coverEmptySpaceUsingExtraField;\n\n  /** Should files be automatically sorted when updating? */\n  private boolean autoSortFiles;\n\n  /** Verify log factory to use. */\n  private final Supplier<VerifyLog> verifyLogFactory;\n\n  /** Verify log to use. */\n  private final VerifyLog verifyLog;\n\n  /** Should skip expensive validation? */\n  private final boolean skipValidation;\n\n  /**\n   * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. This\n   * may happen, for example, if the zip has been changed and the Central Directory and EOCD have\n   * been deleted (in-memory). In that case, this field will save the comment to place on the EOCD\n   * once it is created.\n   *\n   * <p>This field will only be non-{@code null} if there is no in-memory EOCD structure\n   * (<i>i.e.</i>, {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then the\n   * comment will be there instead of being in this field.\n   */\n  @Nullable private byte[] eocdComment;\n\n  /** Is the file in read-only mode? In read-only mode no changes are allowed. */\n  private boolean readOnly;\n\n  /**\n   * Creates a new zip file. If the zip file does not exist, then no file is created at this point\n   * and {@code ZFile} will contain an empty structure. However, an (empty) zip file will be created\n   * if either {@link #update()} or {@link #close()} are used. If a zip file exists, it will be\n   * parsed and read.\n   *\n   * @param file the zip file\n   * @throws IOException some file exists but could not be read\n   * @deprecated use {@link ZFile#openReadOnly(File)} or {@link ZFile#openReadWrite(File)}\n   */\n  @Deprecated\n  public ZFile(File file) throws IOException {\n    this(file, new ZFileOptions());\n  }\n\n  /**\n   * Creates a new zip file. If the zip file does not exist, then no file is created at this point\n   * and {@code ZFile} will contain an empty structure. However, an (empty) zip file will be created\n   * if either {@link #update()} or {@link #close()} are used. If a zip file exists, it will be\n   * parsed and read.\n   *\n   * @param file the zip file\n   * @param options configuration options\n   * @throws IOException some file exists but could not be read\n   * @deprecated use {@link ZFile#openReadOnly(File, ZFileOptions)} or {@link\n   *     ZFile#openReadWrite(File, ZFileOptions)}\n   */\n  @Deprecated\n  public ZFile(File file, ZFileOptions options) throws IOException {\n    this(file, options, false);\n  }\n\n  /**\n   * Creates a new zip file. If the zip file does not exist, then no file is created at this point\n   * and {@code ZFile} will contain an empty structure. However, an (empty) zip file will be created\n   * if either {@link #update()} or {@link #close()} are used. If a zip file exists, it will be\n   * parsed and read.\n   *\n   * @param file the zip file\n   * @param options configuration options\n   * @param readOnly should the file be open in read-only mode? If {@code true} then the file must\n   *     exist and no methods can be invoked that could potentially change the file\n   * @throws IOException some file exists but could not be read\n   * @deprecated use {@link ZFile#openReadOnly(File, ZFileOptions)} or {@link\n   *     ZFile#openReadWrite(File, ZFileOptions)}\n   */\n  @Deprecated\n  public ZFile(File file, ZFileOptions options, boolean readOnly) throws IOException {\n    this.file = file;\n    map =\n        new FileUseMap(\n            0, options.getCoverEmptySpaceUsingExtraField() ? MINIMUM_EXTRA_FIELD_SIZE : 0);\n    this.readOnly = readOnly;\n    dirty = false;\n    closedControl = null;\n    alignmentRule = options.getAlignmentRule();\n    extensions = Lists.newArrayList();\n    toRun = Lists.newArrayList();\n    noTimestamps = options.getNoTimestamps();\n    storage = options.getStorageFactory().create();\n    compressor = options.getCompressor();\n    coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField();\n    autoSortFiles = options.getAutoSortFiles();\n    verifyLogFactory = options.getVerifyLogFactory();\n    verifyLog = verifyLogFactory.get();\n    skipValidation = options.getSkipValidation();\n\n    /*\n     * These two values will be overwritten by openReadOnlyIfClosed() below if the file exists.\n     */\n    state = ZipFileState.CLOSED;\n    raf = null;\n\n    if (file.exists()) {\n      openReadOnlyIfClosed();\n    } else if (readOnly) {\n      throw new IOException(\"File does not exist but read-only mode requested\");\n    } else {\n      dirty = true;\n    }\n\n    entries = Maps.newHashMap();\n    uncompressedEntries = Lists.newArrayList();\n    linkingEntries = Lists.newArrayList();\n    extraDirectoryOffset = 0;\n\n    try {\n      if (state != ZipFileState.CLOSED) {\n        // TODO: to be removed completely once Zip64 is fully supported\n        final long MAX_ENTRY_SIZE = 0xFFFFFFFFL; // 2^32-1\n        long rafSize = raf.length();\n        if (rafSize > MAX_ENTRY_SIZE) {\n          throw new IOException(\"File exceeds size limit of \" + MAX_ENTRY_SIZE + \".\");\n        }\n\n        map.extend(raf.length());\n        readData();\n      }\n\n      // If we don't have an EOCD entry, set the comment to empty.\n      if (eocdEntry == null) {\n        eocdComment = new byte[0];\n      }\n\n      // Notify the extensions if the zip file has been open.\n      if (state != ZipFileState.CLOSED) {\n        notify(ZFileExtension::open);\n      }\n    } catch (Zip64NotSupportedException e) {\n      throw e;\n    } catch (IOException e) {\n      throw new IOException(\"Failed to read zip file '\" + file.getAbsolutePath() + \"'.\", e);\n    } catch (IllegalStateException | IllegalArgumentException | VerifyException e) {\n      throw new RuntimeException(\n          \"Internal error when trying to read zip file '\" + file.getAbsolutePath() + \"'.\", e);\n    }\n  }\n\n  /**\n   * Old name of {@link #openReadOnlyIfClosed()}, method kept for backwards compatibility only.\n   *\n   * @deprecated use {@link #openReadOnlyIfClosed()} if necessary to ensure a {@link ZFile} is open\n   *     and readable\n   */\n  @Deprecated\n  public void openReadOnly() throws IOException {\n    openReadOnlyIfClosed();\n  }\n\n  /**\n   * Opens a new {@link ZFile} from the given file in read-only mode.\n   *\n   * @param file the file to open\n   * @return the created file\n   * @throws IOException failed to read the file\n   */\n  public static ZFile openReadOnly(File file) throws IOException {\n    return openReadOnly(file, new ZFileOptions());\n  }\n\n  /**\n   * Opens a new {@link ZFile} from the given file in read-only mode.\n   *\n   * @param file the file to open\n   * @param options the options to use to open the file; because the file is open read-only, many of\n   *     these options won't have any effect\n   * @return the created file\n   * @throws IOException failed to read the file\n   */\n  public static ZFile openReadOnly(File file, ZFileOptions options) throws IOException {\n    return new ZFile(file, options, true);\n  }\n\n  /**\n   * Opens a new {@link ZFile} from the given file in read-write mode. Opening a file in read-write\n   * mode may force the file to be written even if no changes are made. For example, differences in\n   * signature will force the file to be written. Use {@link #openReadOnly(File, ZFileOptions)} to\n   * open a file and ensure it won't be written.\n   *\n   * <p>The file will be created if it doesn't exist. If the file exists, it must be a valid zip\n   * archive.\n   *\n   * @param file the file to open\n   * @return the created file\n   * @throws IOException failed to read the file\n   */\n  public static ZFile openReadWrite(File file) throws IOException {\n    return openReadWrite(file, new ZFileOptions());\n  }\n\n  /**\n   * Opens a new {@link ZFile} from the given file in read-write mode. Opening a file in read-write\n   * mode may force the file to be written even if no changes are made. For example, differences in\n   * signature will force the file to be written. Use {@link #openReadOnly(File, ZFileOptions)} to\n   * open a file and ensure it won't be written.\n   *\n   * <p>The file will be created if it doesn't exist. If the file exists, it must be a valid zip\n   * archive.\n   *\n   * @param file the file to open\n   * @param options the options to use to open the file\n   * @return the created file\n   * @throws IOException failed to read the file\n   */\n  public static ZFile openReadWrite(File file, ZFileOptions options) throws IOException {\n    return new ZFile(file, options, false);\n  }\n\n  public boolean getSkipValidation() {\n    return skipValidation;\n  }\n\n  /**\n   * Obtains all entries in the file. Entries themselves may be or not written in disk. However, all\n   * of them can be open for reading.\n   *\n   * @return all entries in the zip\n   */\n  public Set<StoredEntry> entries() {\n    Map<String, StoredEntry> entries = Maps.newHashMap();\n\n    for (FileUseMapEntry<StoredEntry> mapEntry : this.entries.values()) {\n      StoredEntry entry = mapEntry.getStore();\n      Preconditions.checkNotNull(entry, \"Entry at %s is null\", mapEntry.getStart());\n      entries.put(entry.getCentralDirectoryHeader().getName(), entry);\n    }\n\n    /*\n     * mUncompressed may override mEntriesReady as we may not have yet processed all\n     * entries.\n     */\n    for (StoredEntry uncompressed : uncompressedEntries) {\n      entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed);\n    }\n\n    for (StoredEntry linking: linkingEntries) {\n      entries.put(linking.getCentralDirectoryHeader().getName(), linking);\n    }\n\n    return Sets.newHashSet(entries.values());\n  }\n\n  /**\n   * Obtains an entry at a given path in the zip.\n   *\n   * @param path the path\n   * @return the entry at the path or {@code null} if none exists\n   */\n  @Nullable\n  public StoredEntry get(String path) {\n    /*\n     * The latest entries are the last ones in uncompressed and they may eventually override\n     * files in entries.\n     */\n    for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) {\n      if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) {\n        return stillUncompressed;\n      }\n    }\n\n    FileUseMapEntry<StoredEntry> found = entries.get(path);\n    if (found == null) {\n      return null;\n    }\n\n    return found.getStore();\n  }\n\n  /**\n   * Reads all the data in the zip file, except the contents of the entries themselves. This method\n   * will populate the directory and maps in the instance variables.\n   *\n   * @throws IOException failed to read the zip file\n   */\n  private void readData() throws IOException {\n    Preconditions.checkState(state != ZipFileState.CLOSED, \"state == ZipFileState.CLOSED\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n\n    readEocd();\n    readCentralDirectory();\n\n    /*\n     * Go over all files and create the usage map, verifying there is no overlap in the files.\n     */\n    long entryEndOffset;\n    long directoryStartOffset;\n\n    if (directoryEntry != null) {\n      CentralDirectory directory = directoryEntry.getStore();\n      Preconditions.checkNotNull(directory, \"Central directory is null\");\n\n      entryEndOffset = 0;\n\n      for (StoredEntry entry : directory.getEntries().values()) {\n        long start = entry.getCentralDirectoryHeader().getOffset();\n        long end = start + entry.getInFileSize();\n\n        /*\n         * If isExtraAlignmentBlock(entry.getLocalExtra()) is true, we know the entry\n         * has an extra field that is solely used for alignment. This means the\n         * actual entry could start at start + extra.length and leave space before.\n         *\n         * But, if we did this here, we would be modifying the zip file and that is\n         * weird because we're just opening it for reading.\n         *\n         * The downside is that we will never reuse that space. Maybe one day ZFile\n         * can be clever enough to remove the local extra when we start modifying the zip\n         * file.\n         */\n\n        Verify.verify(start >= 0, \"start < 0\");\n        Verify.verify(end < map.size(), \"end >= map.size()\");\n\n        FileUseMapEntry<?> found = map.at(start);\n        Verify.verifyNotNull(found);\n\n        // We've got a problem if the found entry is not free or is a free entry but\n        // doesn't cover the whole file.\n        if (!found.isFree() || found.getEnd() < end) {\n          if (found.isFree()) {\n            found = map.after(found);\n            Verify.verify(found != null && !found.isFree());\n          }\n\n          Object foundEntry = found.getStore();\n          Verify.verify(foundEntry != null);\n\n          // Obtains a custom description of an entry.\n          IOExceptionFunction<StoredEntry, String> describe =\n              e ->\n                  String.format(\n                      \"'%s' (offset: %d, size: %d)\",\n                      e.getCentralDirectoryHeader().getName(),\n                      e.getCentralDirectoryHeader().getOffset(),\n                      e.getInFileSize());\n\n          String overlappingEntryDescription;\n          if (foundEntry instanceof StoredEntry) {\n            StoredEntry foundStored = (StoredEntry) foundEntry;\n            overlappingEntryDescription = describe.apply(foundStored);\n          } else {\n            overlappingEntryDescription =\n                \"Central Directory / EOCD: \" + found.getStart() + \" - \" + found.getEnd();\n          }\n\n          throw new IOException(\n              \"Cannot read entry \"\n                  + describe.apply(entry)\n                  + \" because it overlaps with \"\n                  + overlappingEntryDescription);\n        }\n\n        FileUseMapEntry<StoredEntry> mapEntry = map.add(start, end, entry);\n        entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);\n\n        if (end > entryEndOffset) {\n          entryEndOffset = end;\n        }\n      }\n\n      directoryStartOffset = directoryEntry.getStart();\n    } else {\n      /*\n       * No directory means an empty zip file. Use the start of the EOCD to compute\n       * an existing offset.\n       */\n      Verify.verifyNotNull(eocdEntry);\n      Preconditions.checkNotNull(eocdEntry, \"EOCD is null\");\n      directoryStartOffset = eocdEntry.getStart();\n      entryEndOffset = 0;\n    }\n\n    /*\n     * Check if there is an extra central directory offset. If there is, save it. Note that\n     * we can't call extraDirectoryOffset() because that would mark the file as dirty.\n     */\n    long extraOffset = directoryStartOffset - entryEndOffset;\n    Verify.verify(extraOffset >= 0, \"extraOffset (%s) < 0\", extraOffset);\n    extraDirectoryOffset = extraOffset;\n  }\n\n  /**\n   * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable.\n   *\n   * @throws IOException failed to read the EOCD\n   */\n  private void readEocd() throws IOException {\n    Preconditions.checkState(state != ZipFileState.CLOSED, \"state == ZipFileState.CLOSED\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n\n    /*\n     * Read the last part of the zip into memory. If we don't find the EOCD signature by then,\n     * the file is corrupt.\n     */\n    int lastToRead = LAST_BYTES_TO_READ;\n    if (lastToRead > raf.length()) {\n      lastToRead = Ints.checkedCast(raf.length());\n    }\n\n    byte[] last = new byte[lastToRead];\n    directFullyRead(raf.length() - lastToRead, last);\n\n    /*\n     * Start endIdx at the first possible location where the signature can be located and then\n     * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the\n     * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE.\n     *\n     * Because the EOCD signature may exist in the file comment, when we find a signature we\n     * will try to read the Eocd. If we fail, we continue searching for the signature. However,\n     * we will keep the last exception in case we don't find any signature.\n     */\n    Eocd eocd = null;\n    int foundEocdSignature = -1;\n    IOException errorFindingSignature = null;\n    long eocdStart = -1;\n\n    for (int endIdx = last.length - MIN_EOCD_SIZE;\n        endIdx >= 0 && foundEocdSignature == -1;\n        endIdx--) {\n      /*\n       * Remember: little endian...\n       */\n      if (last[endIdx] == EOCD_SIGNATURE[3]\n          && last[endIdx + 1] == EOCD_SIGNATURE[2]\n          && last[endIdx + 2] == EOCD_SIGNATURE[1]\n          && last[endIdx + 3] == EOCD_SIGNATURE[0]) {\n\n        /*\n         * We found a signature. Try to read the EOCD record.\n         */\n\n        foundEocdSignature = endIdx;\n        ByteBuffer eocdBytes =\n            ByteBuffer.wrap(last, foundEocdSignature, last.length - foundEocdSignature);\n\n        try {\n          eocd = new Eocd(eocdBytes);\n          eocdStart = raf.length() - lastToRead + foundEocdSignature;\n\n          /*\n           * Make sure the EOCD takes the whole file up to the end. Log an error if it\n           * doesn't.\n           */\n          if (eocdStart + eocd.getEocdSize() != raf.length()) {\n            verifyLog.log(\n                \"EOCD starts at \"\n                    + eocdStart\n                    + \" and has \"\n                    + eocd.getEocdSize()\n                    + \" bytes, but file ends at \"\n                    + raf.length()\n                    + \".\");\n          }\n        } catch (IOException e) {\n          if (errorFindingSignature != null) {\n            e.addSuppressed(errorFindingSignature);\n          }\n\n          errorFindingSignature = e;\n          foundEocdSignature = -1;\n          eocd = null;\n        }\n      }\n    }\n\n    if (foundEocdSignature == -1) {\n      throw new IOException(\n          \"EOCD signature not found in the last \" + lastToRead + \" bytes of the file.\",\n          errorFindingSignature);\n    }\n\n    Verify.verify(eocdStart >= 0);\n\n    /*\n     * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64\n     * file and we do not support it.\n     */\n    long zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE;\n    if (zip64LocatorStart >= 0) {\n      byte[] possibleZip64Locator = new byte[4];\n      directFullyRead(zip64LocatorStart, possibleZip64Locator);\n      if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator))\n          == ZIP64_EOCD_LOCATOR_SIGNATURE) {\n        throw new Zip64NotSupportedException(\n            \"Zip64 EOCD locator found but Zip64 format is not supported.\");\n      }\n    }\n\n    eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);\n  }\n\n  /**\n   * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This\n   * method can only be called after the EOCD has been read. If the central directory is empty (if\n   * there are no files on the zip archive), then {@link #directoryEntry} will be set to {@code\n   * null}.\n   *\n   * @throws IOException failed to read the central directory\n   */\n  private void readCentralDirectory() throws IOException {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n    Preconditions.checkNotNull(eocdEntry.getStore(), \"eocdEntry.getStore() == null\");\n    Preconditions.checkState(state != ZipFileState.CLOSED, \"state == ZipFileState.CLOSED\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n    Preconditions.checkState(directoryEntry == null, \"directoryEntry != null\");\n\n    Eocd eocd = eocdEntry.getStore();\n\n    long dirSize = eocd.getDirectorySize();\n    if (dirSize > Integer.MAX_VALUE) {\n      throw new IOException(\"Cannot read central directory with size \" + dirSize + \".\");\n    }\n\n    long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize;\n    if (centralDirectoryEnd != eocdEntry.getStart()) {\n      String msg =\n          \"Central directory is stored in [\"\n              + eocd.getDirectoryOffset()\n              + \" - \"\n              + (centralDirectoryEnd - 1)\n              + \"] and EOCD starts at \"\n              + eocdEntry.getStart()\n              + \".\";\n\n      /*\n       * If there is an empty space between the central directory and the EOCD, we proceed\n       * logging an error. If the central directory ends after the start of the EOCD (and\n       * therefore, they overlap), throw an exception.\n       */\n      if (centralDirectoryEnd > eocdEntry.getSize()) {\n        throw new IOException(msg);\n      } else {\n        verifyLog.log(msg);\n      }\n    }\n\n    byte[] directoryData = new byte[Ints.checkedCast(dirSize)];\n    directFullyRead(eocd.getDirectoryOffset(), directoryData);\n\n    CentralDirectory directory =\n        CentralDirectory.makeFromData(\n            ByteBuffer.wrap(directoryData), eocd.getTotalRecords(), this, storage);\n    if (eocd.getDirectorySize() > 0) {\n      directoryEntry =\n          map.add(\n              eocd.getDirectoryOffset(),\n              eocd.getDirectoryOffset() + eocd.getDirectorySize(),\n              directory);\n    }\n  }\n\n  /**\n   * Opens a portion of the zip for reading. The zip must be open for this method to be invoked.\n   * Note that if the zip has not been updated, the individual zip entries may not have been written\n   * yet.\n   *\n   * @param start the index within the zip file to start reading\n   * @param end the index within the zip file to end reading (the actual byte pointed by\n   *     <em>end</em> will not be read)\n   * @return a stream that will read the portion of the file; no decompression is done, data is\n   *     returned <em>as is</em>\n   * @throws IOException failed to open the zip file\n   */\n  public InputStream directOpen(final long start, final long end) throws IOException {\n    Preconditions.checkState(state != ZipFileState.CLOSED, \"state == ZipFileState.CLOSED\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n    Preconditions.checkArgument(start >= 0, \"start < 0\");\n    Preconditions.checkArgument(end >= start, \"end < start\");\n    Preconditions.checkArgument(end <= raf.length(), \"end > raf.length()\");\n\n    return new InputStream() {\n      private long mCurr = start;\n\n      @Override\n      public int read() throws IOException {\n        if (mCurr == end) {\n          return -1;\n        }\n\n        byte[] b = new byte[1];\n        int r = directRead(mCurr, b);\n        if (r > 0) {\n          mCurr++;\n          return b[0];\n        } else {\n          return -1;\n        }\n      }\n\n      @Override\n      public int read(byte[] b, int off, int len) throws IOException {\n        Preconditions.checkNotNull(b, \"b == null\");\n        Preconditions.checkArgument(off >= 0, \"off < 0\");\n        Preconditions.checkArgument(off <= b.length, \"off > b.length\");\n        Preconditions.checkArgument(len >= 0, \"len < 0\");\n        Preconditions.checkArgument(off + len <= b.length, \"off + len > b.length\");\n\n        long availableToRead = end - mCurr;\n        long toRead = Math.min(len, availableToRead);\n\n        if (toRead == 0) {\n          return -1;\n        }\n\n        if (toRead > Integer.MAX_VALUE) {\n          throw new IOException(\"Cannot read \" + toRead + \" bytes.\");\n        }\n\n        int r = directRead(mCurr, b, off, Ints.checkedCast(toRead));\n        if (r > 0) {\n          mCurr += r;\n        }\n\n        return r;\n      }\n    };\n  }\n\n  /**\n   * Deletes an entry from the zip. This method does not actually delete anything on disk. It just\n   * changes in-memory structures. Use {@link #update()} to update the contents on disk.\n   *\n   * @param entry the entry to delete\n   * @param notify should listeners be notified of the deletion? This will only be {@code false} if\n   *     the entry is being removed as part of a replacement\n   * @throws IOException failed to delete the entry\n   * @throws IllegalStateException if open in read-only mode\n   */\n  void delete(final StoredEntry entry, boolean notify) throws IOException {\n    checkNotInReadOnlyMode();\n\n    String path = entry.getCentralDirectoryHeader().getName();\n    FileUseMapEntry<StoredEntry> mapEntry = entries.get(path);\n    Preconditions.checkNotNull(mapEntry, \"mapEntry == null\");\n    Preconditions.checkArgument(entry == mapEntry.getStore(), \"entry != mapEntry.getStore()\");\n\n    dirty = true;\n\n    map.remove(mapEntry);\n    entries.remove(path);\n\n    if (notify) {\n      notify(ext -> ext.removed(entry));\n    }\n  }\n\n  /**\n   * Checks that the file is not in read-only mode.\n   *\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  private void checkNotInReadOnlyMode() {\n    if (readOnly) {\n      throw new IllegalStateException(\"Illegal operation in read only model\");\n    }\n  }\n\n  /**\n   * Updates the file writing new entries and removing deleted entries. This will force reopening\n   * the file as read/write if the file wasn't open in read/write mode.\n   *\n   * @throws IOException failed to update the file; this exception may have been thrown by the\n   *     compressor but only reported here\n   */\n  public void update() throws IOException {\n    checkNotInReadOnlyMode();\n\n    /*\n     * Process all background stuff before calling in the extensions.\n     */\n    processAllReadyEntriesWithWait();\n    notify(ZFileExtension::beforeUpdate);\n\n    /*\n     * Process all background stuff that may be leftover by the extensions.\n     */\n    processAllReadyEntriesWithWait();\n\n    if (dirty) {\n      writeAllFilesToZip();\n    }\n\n    // Even if no files were modified, we still need to recompute the central directory and EOCD\n    // in case they have been modified by any extension.\n    recomputeAndWriteCentralDirectoryAndEocd();\n\n    // If there are no changes to the file, we may get here without even opening the zip as a\n    // RandomAccessFile. In that case, don't try to change the size since we're sure there are no\n    // changes.\n    if (raf != null) {\n      // Ensure we make the zip have the right size (only useful if shrinking), mark the zip as\n      // no longer dirty and notify all extensions.\n      if (raf.length() != map.size()) {\n        raf.setLength(map.size());\n      }\n    }\n\n    // Regardless of whether the zip was dirty or not, we're sure it isn't now.\n    dirty = false;\n\n    notify(\n        ext -> {\n          ext.updated();\n          return null;\n        });\n  }\n\n  /**\n   * Writes all files to the zip, sorting/packing if necessary. The central directory and EOCD are\n   * deleted. When this method finishes, all entries have been written to the file and are properly\n   * aligned.\n   */\n  private void writeAllFilesToZip() throws IOException {\n    reopenRw();\n\n    /*\n     * At this point, no more files can be added. We may need to repack to remove extra\n     * empty spaces or sort. If we sort, we don't need to repack as sorting forces the\n     * zip file to be as compact as possible.\n     */\n    if (autoSortFiles) {\n      sortZipContents();\n    } else {\n      packIfNecessary();\n    }\n\n    /*\n     * We're going to change the file so delete the central directory and the EOCD as they\n     * will have to be rewritten.\n     */\n    deleteDirectoryAndEocd();\n    map.truncate();\n\n    /*\n     * If we need to use the extra field to cover empty spaces, we do the processing here.\n     */\n    if (coverEmptySpaceUsingExtraField) {\n\n      /* We will go over all files in the zip and check whether there is empty space before\n       * them. If there is, then we will move the entry to the beginning of the empty space\n       * (covering it) and extend the extra field with the size of the empty space.\n       */\n      for (FileUseMapEntry<StoredEntry> entry : new HashSet<>(entries.values())) {\n        StoredEntry storedEntry = entry.getStore();\n        Preconditions.checkNotNull(storedEntry, \"Entry at %s is null\", entry.getStart());\n\n        FileUseMapEntry<?> before = map.before(entry);\n        if (before == null || !before.isFree()) {\n          continue;\n        }\n\n        /*\n         * We have free space before the current entry. However, we do know that it can\n         * be covered by the extra field, because both sortZipContents() and\n         * packIfNecessary() guarantee it.\n         */\n        int localExtraSize =\n            storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize());\n        Verify.verify(localExtraSize <= MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE);\n\n        /*\n         * Move file back in the zip.\n         */\n        storedEntry.loadSourceIntoMemory();\n\n        long newStart = before.getStart();\n        long newSize = entry.getSize() + before.getSize();\n\n        /*\n         * Remove the entry.\n         */\n        String name = storedEntry.getCentralDirectoryHeader().getName();\n        map.remove(entry);\n        Verify.verify(entry == entries.remove(name));\n\n        /*\n         * Make a list will all existing segments in the entry's extra field, but remove\n         * the alignment field, if it exists. Also, sum the size of all kept extra field\n         * segments.\n         */\n        ImmutableList<ExtraField.Segment> currentSegments;\n        try {\n          currentSegments = storedEntry.getLocalExtra().getSegments();\n        } catch (IOException e) {\n          /*\n           * Parsing current segments has failed. This means the contents of the extra\n           * field are not valid. We'll continue discarding the existing segments.\n           */\n          currentSegments = ImmutableList.of();\n        }\n\n        List<ExtraField.Segment> extraFieldSegments = new ArrayList<>();\n        int newExtraFieldSize = 0;\n        for (ExtraField.Segment segment : currentSegments) {\n          if (segment.getHeaderId() != ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {\n            extraFieldSegments.add(segment);\n            newExtraFieldSize += segment.size();\n          }\n        }\n\n        int spaceToFill =\n            Ints.checkedCast(\n                before.getSize() + storedEntry.getLocalExtra().size() - newExtraFieldSize);\n\n        extraFieldSegments.add(\n            new ExtraField.AlignmentSegment(chooseAlignment(storedEntry), spaceToFill));\n\n        storedEntry.setLocalExtraNoNotify(new ExtraField(ImmutableList.copyOf(extraFieldSegments)));\n        entries.put(name, map.add(newStart, newStart + newSize, storedEntry));\n\n        /*\n         * Reset the offset to force the file to be rewritten.\n         */\n        storedEntry.getCentralDirectoryHeader().setOffset(-1);\n      }\n    }\n\n    /*\n     * Write new files in the zip. We identify new files because they don't have an offset\n     * in the zip where they are written although we already know, by their location in the\n     * file map, where they will be written to.\n     *\n     * Before writing the files, we sort them in the order they are written in the file so that\n     * writes are made in order on disk.\n     * This is, however, unlikely to optimize anything relevant given the way the Operating\n     * System does caching, but it certainly won't hurt :)\n     */\n    TreeMap<FileUseMapEntry<?>, StoredEntry> toWriteToStore =\n        new TreeMap<>(FileUseMapEntry.COMPARE_BY_START);\n\n    for (FileUseMapEntry<StoredEntry> entry : entries.values()) {\n      StoredEntry entryStore = entry.getStore();\n      Preconditions.checkNotNull(entryStore, \"Entry at %s is null\", entry.getStart());\n      if (entryStore.getCentralDirectoryHeader().getOffset() == -1) {\n        toWriteToStore.put(entry, entryStore);\n      }\n    }\n\n    /*\n     * Add all free entries to the set.\n     */\n    for (FileUseMapEntry<?> freeArea : map.getFreeAreas()) {\n      toWriteToStore.put(freeArea, null);\n    }\n\n    /*\n     * Write everything to file.\n     */\n    byte[] chunk = new byte[IO_BUFFER_SIZE];\n    for (FileUseMapEntry<?> fileUseMapEntry : toWriteToStore.keySet()) {\n      StoredEntry entry = toWriteToStore.get(fileUseMapEntry);\n      if (entry == null) {\n        int size = Ints.checkedCast(fileUseMapEntry.getSize());\n        directWrite(fileUseMapEntry.getStart(), new byte[size]);\n      } else {\n        writeEntry(entry, fileUseMapEntry.getStart(), chunk);\n      }\n    }\n  }\n\n  /**\n   * Recomputes the central directory and EOCD and notifies extensions that all entries have been\n   * written. Extensions may further modify the archive and this may require the directory and EOCD\n   * to be recomputed several times.\n   *\n   * <p>This method finishes when the central directory and EOCD have both been computed and written\n   * to the zip file and all extensions have been notified using {@link\n   * ZFileExtension#entriesWritten()}.\n   */\n  private void recomputeAndWriteCentralDirectoryAndEocd() throws IOException {\n    boolean changedAnything = false;\n    boolean hasCentralDirectory;\n    int extensionBugDetector = MAXIMUM_EXTENSION_CYCLE_COUNT;\n    do {\n      // Try to compute the central directory and EOCD. Computing the central directory may end\n      // with directoryEntry == null if there are no entries in the zip.\n      if (directoryEntry == null) {\n        reopenRw();\n        changedAnything = true;\n        computeCentralDirectory();\n      }\n\n      if (eocdEntry == null) {\n        // It is fine to call computeEocd even if directoryEntry == null as long as the zip has\n        // no files.\n        reopenRw();\n        changedAnything = true;\n        computeEocd();\n      }\n\n      hasCentralDirectory = (directoryEntry != null);\n\n      notify(\n          ext -> {\n            ext.entriesWritten();\n            return null;\n          });\n\n      if ((--extensionBugDetector) == 0) {\n        throw new IOException(\n            \"Extensions keep resetting the central directory. This is \" + \"probably a bug.\");\n      }\n    } while ((hasCentralDirectory && directoryEntry == null) || eocdEntry == null);\n\n    if (changedAnything) {\n      reopenRw();\n      appendCentralDirectory();\n      appendEocd();\n    }\n  }\n\n  /**\n   * Reorganizes the zip so that there are no gaps between files bigger than {@link\n   * #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField} is set to\n   * {@code true}.\n   *\n   * <p>Essentially, this makes sure we can cover any empty space with the extra field, given that\n   * the local extra field is limited to {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE}. If an entry\n   * is too far from the previous one, it is removed and re-added.\n   *\n   * @throws IOException failed to repack\n   */\n  private void packIfNecessary() throws IOException {\n    if (!coverEmptySpaceUsingExtraField) {\n      return;\n    }\n\n    SortedSet<FileUseMapEntry<StoredEntry>> entriesByLocation =\n        new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);\n    entriesByLocation.addAll(entries.values());\n\n    for (FileUseMapEntry<StoredEntry> entry : entriesByLocation) {\n      StoredEntry storedEntry = entry.getStore();\n      Preconditions.checkNotNull(storedEntry, \"Entry at %s is null\", entry.getStart());\n\n      FileUseMapEntry<?> before = map.before(entry);\n      if (before == null || !before.isFree()) {\n        continue;\n      }\n\n      int localExtraSize = storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize());\n      if (localExtraSize > MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) {\n        /*\n         * This entry is too far from the previous one. Remove it and re-add it to the\n         * zip file.\n         */\n        reAdd(storedEntry, PositionHint.LOWEST_OFFSET);\n      }\n    }\n  }\n\n  /**\n   * Removes a stored entry from the zip and adds it back again. This will force the entry to be\n   * loaded into memory and repositioned in the zip file. It will also mark the archive as being\n   * dirty.\n   *\n   * @param entry the entry\n   * @param positionHint hint to where the file should be positioned when re-adding\n   * @throws IOException failed to load the entry into memory\n   */\n  private void reAdd(StoredEntry entry, PositionHint positionHint) throws IOException {\n    String name = entry.getCentralDirectoryHeader().getName();\n    FileUseMapEntry<StoredEntry> mapEntry = entries.get(name);\n    Preconditions.checkNotNull(mapEntry);\n    Preconditions.checkState(mapEntry.getStore() == entry);\n\n    entry.loadSourceIntoMemory();\n\n    map.remove(mapEntry);\n    entries.remove(name);\n    FileUseMapEntry<StoredEntry> positioned = positionInFile(entry, positionHint);\n    entries.put(name, positioned);\n    dirty = true;\n  }\n\n  /**\n   * Invoked from {@link StoredEntry} when entry has changed in a way that forces the local header\n   * to be rewritten\n   *\n   * @param entry the entry that changed\n   * @param resized was the local header resized?\n   * @throws IOException failed to load the entry into memory\n   */\n  void localHeaderChanged(StoredEntry entry, boolean resized) throws IOException {\n    dirty = true;\n\n    if (resized) {\n      reAdd(entry, PositionHint.ANYWHERE);\n    }\n  }\n\n  /** Invoked when the central directory has changed and needs to be rewritten. */\n  void centralDirectoryChanged() {\n    dirty = true;\n    deleteDirectoryAndEocd();\n  }\n\n  /** Updates the file and closes it. */\n  @Override\n  public void close() throws IOException {\n    // We need to make sure to release raf, otherwise we end up locking the file on\n    // Windows. Use try-with-resources to handle exception suppressing.\n    try (Closeable ignored = this::innerClose) {\n      if (!readOnly) {\n        update();\n      }\n\n      storage.close();\n    }\n\n    notify(\n        ext -> {\n          ext.closed();\n          return null;\n        });\n  }\n\n  /**\n   * Removes the Central Directory and EOCD from the file. This will free space for new entries as\n   * well as allowing the zip file to be truncated if files have been removed.\n   *\n   * <p>This method does not mark the zip as dirty.\n   */\n  private void deleteDirectoryAndEocd() {\n    if (directoryEntry != null) {\n      map.remove(directoryEntry);\n      directoryEntry = null;\n    }\n\n    if (eocdEntry != null) {\n      map.remove(eocdEntry);\n\n      Eocd eocd = eocdEntry.getStore();\n      Verify.verify(eocd != null);\n      eocdComment = eocd.getComment();\n      eocdEntry = null;\n    }\n  }\n\n  /**\n   * Writes an entry's data in the zip file. This includes everything: the local header and the data\n   * itself. After writing, the entry is updated with the offset and its source replaced with a\n   * source that reads from the zip file.\n   *\n   * @param entry the entry to write\n   * @param offset the offset at which the entry should be written\n   * @throws IOException failed to write the entry\n   */\n  private void writeEntry(StoredEntry entry, long offset, byte[] chunk) throws IOException {\n    Preconditions.checkArgument(\n        entry.getDataDescriptorType() == DataDescriptorType.NO_DATA_DESCRIPTOR,\n        \"Cannot write entries with a data \" + \"descriptor.\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n    Preconditions.checkState(state == ZipFileState.OPEN_RW, \"state != ZipFileState.OPEN_RW\");\n\n    int r;\n    // Put header data to the beginning of buffer\n    // LSPatch: write extra entries in the extra field if it's a linking\n    int localHeaderSize = entry.getLocalHeaderSize();\n    for (var segment : entry.getLocalExtra().getSegments()) {\n      if (segment instanceof ExtraField.LinkingEntrySegment) {\n        ((ExtraField.LinkingEntrySegment) segment).setOffset(localHeaderSize, offset);\n      }\n    }\n    int readOffset = entry.toHeaderData(chunk);\n    assert localHeaderSize == readOffset;\n    long writeOffset = offset;\n    try (InputStream is = entry.getSource().getRawByteSource().openStream()) {\n      while ((r = is.read(chunk, readOffset, chunk.length - readOffset)) >= 0 || readOffset > 0) {\n        int toWrite = (r == -1 ? 0 : r) + readOffset;\n        directWrite(writeOffset, chunk, 0, toWrite);\n        writeOffset += toWrite;\n        readOffset = 0;\n      }\n    }\n\n    /*\n     * Set the entry's offset and create the entry source.\n     */\n    entry.replaceSourceFromZip(offset);\n  }\n\n  /**\n   * Computes the central directory. The central directory must not have been computed yet. When\n   * this method finishes, the central directory has been computed {@link #directoryEntry}, unless\n   * the directory is empty in which case {@link #directoryEntry} is left as {@code null}. Nothing\n   * is written to disk as a result of this method's invocation.\n   *\n   * @throws IOException failed to append the central directory\n   */\n  private void computeCentralDirectory() throws IOException {\n    Preconditions.checkState(state == ZipFileState.OPEN_RW, \"state != ZipFileState.OPEN_RW\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n    Preconditions.checkState(directoryEntry == null, \"directoryEntry != null\");\n\n    Set<StoredEntry> newStored = Sets.newHashSet();\n    for (FileUseMapEntry<StoredEntry> mapEntry : entries.values()) {\n      newStored.add(mapEntry.getStore());\n    }\n\n    newStored.addAll(linkingEntries);\n\n    /*\n     * Make sure we truncate the map before computing the central directory's location since\n     * the central directory is the last part of the file.\n     */\n    map.truncate();\n\n    CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this);\n    byte[] newDirectoryBytes = newDirectory.toBytes();\n    long directoryOffset = map.size() + extraDirectoryOffset;\n\n    map.extend(directoryOffset + newDirectoryBytes.length);\n\n    if (newDirectoryBytes.length > 0) {\n      directoryEntry =\n          map.add(directoryOffset, directoryOffset + newDirectoryBytes.length, newDirectory);\n    }\n  }\n\n  /**\n   * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be {@code\n   * null} only if there are no files in the archive.\n   *\n   * @throws IOException failed to append the central directory\n   */\n  private void appendCentralDirectory() throws IOException {\n    Preconditions.checkState(state == ZipFileState.OPEN_RW, \"state != ZipFileState.OPEN_RW\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n\n    if (entries.isEmpty()) {\n      Preconditions.checkState(directoryEntry == null, \"directoryEntry != null\");\n      return;\n    }\n\n    Preconditions.checkNotNull(directoryEntry, \"directoryEntry != null\");\n\n    CentralDirectory newDirectory = directoryEntry.getStore();\n    Preconditions.checkNotNull(newDirectory, \"newDirectory != null\");\n\n    byte[] newDirectoryBytes = newDirectory.toBytes();\n    long directoryOffset = directoryEntry.getStart();\n\n    /*\n     * It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend\n     * the file. Even if we do not have any directory data to write, the extend() call below\n     * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at\n     * the beginning.\n     */\n    directWrite(directoryOffset, newDirectoryBytes);\n  }\n\n  /**\n   * Obtains the byte array representation of the central directory. The central directory must have\n   * been already computed. If there are no entries in the zip, the central directory will be empty.\n   *\n   * @return the byte representation, or an empty array if there are no entries in the zip\n   * @throws IOException failed to compute the central directory byte representation\n   */\n  public byte[] getCentralDirectoryBytes() throws IOException {\n    if (entries.isEmpty()) {\n      Preconditions.checkState(directoryEntry == null, \"directoryEntry != null\");\n      return new byte[0];\n    }\n\n    Preconditions.checkNotNull(directoryEntry, \"directoryEntry == null\");\n\n    CentralDirectory cd = directoryEntry.getStore();\n    Preconditions.checkNotNull(cd, \"cd == null\");\n    return cd.toBytes();\n  }\n\n  /**\n   * Computes the EOCD. This creates a new {@link #eocdEntry}. The central directory must already be\n   * written. If {@link #directoryEntry} is {@code null}, then the zip file must not have any\n   * entries.\n   *\n   * @throws IOException failed to write the EOCD\n   */\n  private void computeEocd() throws IOException {\n    Preconditions.checkState(state == ZipFileState.OPEN_RW, \"state != ZipFileState.OPEN_RW\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n    if (directoryEntry == null) {\n      Preconditions.checkState(entries.isEmpty(), \"directoryEntry == null && !entries.isEmpty()\");\n    }\n\n    long dirStart;\n    long dirSize = 0;\n\n    if (directoryEntry != null) {\n      CentralDirectory directory = directoryEntry.getStore();\n\n      Preconditions.checkNotNull(directory, \"Central directory is null\");\n\n      dirStart = directoryEntry.getStart();\n      dirSize = directoryEntry.getSize();\n      Verify.verify(directory.getEntries().size() == entries.size() + linkingEntries.size());\n    } else {\n      /*\n       * If we do not have a directory, then we must leave any requested offset empty.\n       */\n      dirStart = extraDirectoryOffset;\n    }\n\n    Verify.verify(eocdComment != null);\n    Eocd eocd = new Eocd(entries.size() + linkingEntries.size(), dirStart, dirSize, eocdComment);\n    eocdComment = null;\n\n    byte[] eocdBytes = eocd.toBytes();\n    long eocdOffset = map.size();\n\n    map.extend(eocdOffset + eocdBytes.length);\n\n    eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);\n  }\n\n  /**\n   * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The central\n   * directory must already be written. If {@link #directoryEntry} is {@code null}, then the zip\n   * file must not have any entries.\n   *\n   * @throws IOException failed to write the EOCD\n   */\n  private void appendEocd() throws IOException {\n    Preconditions.checkState(state == ZipFileState.OPEN_RW, \"state != ZipFileState.OPEN_RW\");\n    Preconditions.checkNotNull(raf, \"raf == null\");\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    Eocd eocd = eocdEntry.getStore();\n    Preconditions.checkNotNull(eocd, \"eocd == null\");\n\n    byte[] eocdBytes = eocd.toBytes();\n    long eocdOffset = eocdEntry.getStart();\n\n    directWrite(eocdOffset, eocdBytes);\n  }\n\n  /**\n   * Obtains the byte array representation of the EOCD. The EOCD must have already been computed for\n   * this method to be invoked.\n   *\n   * @return the byte representation of the EOCD\n   * @throws IOException failed to obtain the byte representation of the EOCD\n   */\n  public byte[] getEocdBytes() throws IOException {\n    Preconditions.checkNotNull(eocdEntry, \"eocdEntry == null\");\n\n    Eocd eocd = eocdEntry.getStore();\n    Preconditions.checkNotNull(eocd, \"eocd == null\");\n    return eocd.toBytes();\n  }\n\n  /**\n   * Closes the file, if it is open.\n   *\n   * @throws IOException failed to close the file\n   */\n  private void innerClose() throws IOException {\n    if (state == ZipFileState.CLOSED) {\n      return;\n    }\n\n    Verify.verifyNotNull(raf, \"raf == null\");\n\n    raf.close();\n    raf = null;\n    state = ZipFileState.CLOSED;\n    if (closedControl == null) {\n      closedControl = new CachedFileContents<>(file);\n    }\n\n    closedControl.closed(null);\n  }\n\n  /**\n   * If the zip file is closed, opens it in read-only mode. If it is already open, does nothing. In\n   * general, it is not necessary to directly invoke this method. However, if directly reading the\n   * zip file using, for example {@link #directRead(long, byte[])}, then this method needs to be\n   * called.\n   *\n   * @throws IOException failed to open the file\n   */\n  public void openReadOnlyIfClosed() throws IOException {\n    if (state != ZipFileState.CLOSED) {\n      return;\n    }\n\n    state = ZipFileState.OPEN_RO;\n    raf = new RandomAccessFile(file, \"r\");\n  }\n\n  /**\n   * Opens (or reopens) the zip file as read-write. This method will ensure that {@link #raf} is not\n   * null and open for writing.\n   *\n   * @throws IOException failed to open the file, failed to close it or the file was closed and has\n   *     been modified outside the control of this object\n   */\n  private void reopenRw() throws IOException {\n    // We an never open a file RW in read-only mode. We should never get this far, though.\n    Verify.verify(!readOnly);\n\n    if (state == ZipFileState.OPEN_RW) {\n      return;\n    }\n\n    boolean wasClosed;\n    if (state == ZipFileState.OPEN_RO) {\n      /*\n       * ReadAccessFile does not have a way to reopen as RW so we have to close it and\n       * open it again.\n       */\n      innerClose();\n      wasClosed = false;\n    } else {\n      wasClosed = true;\n    }\n\n    Verify.verify(state == ZipFileState.CLOSED, \"state != ZpiFileState.CLOSED\");\n    Verify.verify(raf == null, \"raf != null\");\n\n    if (closedControl != null && !closedControl.isValid()) {\n      throw new IOException(\n          \"File '\"\n              + file.getAbsolutePath()\n              + \"' has been modified \"\n              + \"by an external application.\");\n    }\n\n    raf = new RandomAccessFile(file, \"rw\");\n    state = ZipFileState.OPEN_RW;\n\n    /*\n     * Now that we've open the zip and are ready to write, clear out any data descriptors\n     * in the zip since we don't need them and they take space in the archive.\n     */\n    for (StoredEntry entry : entries()) {\n      dirty |= entry.removeDataDescriptor();\n    }\n\n    if (wasClosed) {\n      notify(ZFileExtension::open);\n    }\n  }\n\n  /**\n   * Equivalent to call {@link #add(String, InputStream, boolean)} using {@code true} as {@code\n   * mayCompress}.\n   *\n   * @param name the file name (<em>i.e.</em>, path); paths should be defined using slashes and the\n   *     name should not end in slash\n   * @param stream the source for the file's data\n   * @throws IOException failed to read the source data\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void add(String name, InputStream stream) throws IOException {\n    checkNotInReadOnlyMode();\n    add(name, stream, true);\n  }\n\n  /**\n   * Adds a file to the archive.\n   *\n   * <p>Adding the file will not update the archive immediately. Updating will only happen when the\n   * {@link #update()} method is invoked.\n   *\n   * <p>Adding a file with the same name as an existing file will replace that file in the archive.\n   *\n   * @param name the file name (<em>i.e.</em>, path); paths should be defined using slashes and the\n   *     name should not end in slash\n   * @param stream the source for the file's data\n   * @param mayCompress can the file be compressed? This flag will be ignored if the alignment rules\n   *     force the file to be aligned, in which case the file will not be compressed.\n   * @throws IOException failed to read the source data\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public StoredEntry add(String name, InputStream stream, boolean mayCompress) throws IOException {\n    return add(name, storage.fromStream(stream), mayCompress);\n  }\n\n  /**\n   * Adds a file to the archive.\n   *\n   * <p>Adding the file will not update the archive immediately. Updating will only happen when the\n   * {@link #update()} method is invoked.\n   *\n   * <p>Adding a file with the same name as an existing file will replace that file in the archive.\n   *\n   * @param name the file name (<em>i.e.</em>, path); paths should be defined using slashes and the\n   *     name should not end in slash\n   * @param source the source for the file's data\n   * @param mayCompress can the file be compressed? This flag will be ignored if the alignment rules\n   *     force the file to be aligned, in which case the file will not be compressed.\n   * @throws IOException failed to read the source data\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void add(String name, ByteSource source, boolean mayCompress) throws IOException {\n    Optional<Long> sizeBytes = source.sizeIfKnown();\n    if (!sizeBytes.isPresent()) {\n      throw new IllegalArgumentException(\"Can only add ByteSources with known size\");\n    }\n    add(name, new CloseableDelegateByteSource(source, sizeBytes.get()), mayCompress);\n  }\n\n  private StoredEntry add(String name, CloseableByteSource source, boolean mayCompress)\n      throws IOException {\n    checkNotInReadOnlyMode();\n\n    /*\n     * Clean pending background work, if needed.\n     */\n    processAllReadyEntries();\n\n    return add(makeStoredEntry(name, source, mayCompress));\n  }\n\n  public void addLink(StoredEntry linkedEntry, String dstName)\n          throws IOException {\n      addNestedLink(linkedEntry, dstName, null, 0L, false);\n  }\n\n  void addNestedLink(StoredEntry linkedEntry, String dstName, StoredEntry nestedEntry, long nestedOffset, boolean dummy)\n          throws IOException {\n    Preconditions.checkArgument(linkedEntry != null, \"linkedEntry is null\");\n    Preconditions.checkArgument(linkedEntry.getCentralDirectoryHeader().getOffset() < 0, \"linkedEntry is not new file\");\n    Preconditions.checkArgument(!linkedEntry.isLinkingEntry(), \"linkedEntry is a linking entry\");\n    var linkingEntry = new StoredEntry(dstName, this, storage, linkedEntry, nestedEntry, nestedOffset, dummy);\n    linkingEntries.add(linkingEntry);\n    linkedEntry.setLocalExtraNoNotify(new ExtraField(ImmutableList.<ExtraField.Segment>builder().add(linkedEntry.getLocalExtra().getSegments().toArray(new ExtraField.Segment[0])).add(new ExtraField.LinkingEntrySegment(linkingEntry)).build()));\n    reAdd(linkedEntry, PositionHint.LOWEST_OFFSET);\n  }\n\n  public NestedZip addNestedZip(NestedZip.NameCallback name, File src, boolean mayCompress) throws IOException {\n    return new NestedZip(name, this, src, mayCompress);\n  }\n\n\n  /**\n   * Adds a {@link StoredEntry} to the zip. The entry is not immediately added to {@link #entries}\n   * because data may not yet be available. Instead, it is placed under {@link #uncompressedEntries}\n   * and later moved to {@link #processAllReadyEntries()} when done.\n   *\n   * <p>This method invokes {@link #processAllReadyEntries()} to move the entry if it has already\n   * been computed so, if there is no delay in compression, and no more files are in waiting queue,\n   * then the entry is added to {@link #entries} immediately.\n   *\n   * @param newEntry the entry to add\n   * @throws IOException failed to process this entry (or a previous one whose future only completed\n   *     now)\n   */\n  private StoredEntry add(final StoredEntry newEntry) throws IOException {\n    uncompressedEntries.add(newEntry);\n    processAllReadyEntries();\n    return newEntry;\n  }\n\n  /**\n   * Creates a stored entry. This does not add the entry to the zip file, it just creates the {@link\n   * StoredEntry} object.\n   *\n   * @param name the name of the entry\n   * @param source the source with the entry's data\n   * @param mayCompress can the entry be compressed?\n   * @return the created entry\n   * @throws IOException failed to create the entry\n   */\n  private StoredEntry makeStoredEntry(String name, CloseableByteSource source, boolean mayCompress)\n      throws IOException {\n    long crc32 = source.hash(Hashing.crc32()).padToLong();\n\n    boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name);\n\n    SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo = SettableFuture.create();\n    GPFlags flags = GPFlags.make(encodeWithUtf8);\n    CentralDirectoryHeader newFileData =\n        new CentralDirectoryHeader(\n            name, EncodeUtils.encode(name, flags), source.size(), compressInfo, flags, this);\n    newFileData.setCrc32(crc32);\n\n    /*\n     * Create the new entry and sets its data source. Offset should be set to -1 automatically\n     * because this is a new file. With offset set to -1, StoredEntry does not try to verify the\n     * local header. Since this is a new file, there is no local header and not checking it is\n     * what we want to happen.\n     */\n    Verify.verify(newFileData.getOffset() == -1);\n    return new StoredEntry(\n        newFileData, this, createSources(mayCompress, source, compressInfo, newFileData), storage);\n  }\n\n  /**\n   * Creates the processed and raw sources for an entry.\n   *\n   * @param mayCompress can the entry be compressed?\n   * @param source the entry's data (uncompressed)\n   * @param compressInfo the compression info future that will be set when the raw entry is created\n   *     and the {@link CentralDirectoryHeaderCompressInfo} object can be created\n   * @param newFileData the central directory header for the new file\n   * @return the sources whose data may or may not be already defined\n   * @throws IOException failed to create the raw sources\n   */\n  private ProcessedAndRawByteSources createSources(\n      boolean mayCompress,\n      CloseableByteSource source,\n      SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo,\n      CentralDirectoryHeader newFileData)\n      throws IOException {\n    if (mayCompress) {\n      ListenableFuture<CompressionResult> result = compressor.compress(source, storage);\n      Futures.addCallback(\n          result,\n          new FutureCallback<CompressionResult>() {\n            @Override\n            public void onSuccess(CompressionResult result) {\n              compressInfo.set(\n                  new CentralDirectoryHeaderCompressInfo(\n                      newFileData, result.getCompressionMethod(), result.getSize()));\n            }\n\n            @Override\n            public void onFailure(Throwable t) {\n              compressInfo.setException(t);\n            }\n          },\n          MoreExecutors.directExecutor());\n\n      ListenableFuture<CloseableByteSource> compressedByteSourceFuture =\n          Futures.transform(result, CompressionResult::getSource, MoreExecutors.directExecutor());\n      LazyDelegateByteSource compressedByteSource =\n          new LazyDelegateByteSource(compressedByteSourceFuture);\n      return new ProcessedAndRawByteSources(source, compressedByteSource);\n    } else {\n      compressInfo.set(\n          new CentralDirectoryHeaderCompressInfo(\n              newFileData, CompressionMethod.STORE, source.size()));\n      return new ProcessedAndRawByteSources(source, source);\n    }\n  }\n\n  /**\n   * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will stop as\n   * soon as entry whose future has not been completed is found.\n   *\n   * @throws IOException the exception reported in the future computation, if any, or failed to add\n   *     a file to the archive\n   */\n  private void processAllReadyEntries() throws IOException {\n    /*\n     * Many things can happen during addToEntries(). Because addToEntries() fires\n     * notifications to extensions, other files can be added, removed, etc. Ee are *not*\n     * guaranteed that new stuff does not get into uncompressedEntries: add() will still work\n     * and will add new entries in there.\n     *\n     * However -- important -- processReadyEntries() may be invoked during addToEntries()\n     * because of the extension mechanism. This means that stuff *can* be removed from\n     * uncompressedEntries and moved to entries during addToEntries().\n     */\n    while (!uncompressedEntries.isEmpty()) {\n      StoredEntry next = uncompressedEntries.get(0);\n      CentralDirectoryHeader cdh = next.getCentralDirectoryHeader();\n      Future<CentralDirectoryHeaderCompressInfo> compressionInfo = cdh.getCompressionInfo();\n      if (!compressionInfo.isDone()) {\n        /*\n         * First entry in queue is not yet complete. We can't do anything else.\n         */\n        return;\n      }\n\n      uncompressedEntries.remove(0);\n\n      try {\n        compressionInfo.get();\n      } catch (InterruptedException e) {\n        throw new IOException(\n            \"Impossible I/O exception: get for already computed \"\n                + \"future throws InterruptedException\",\n            e);\n      } catch (ExecutionException e) {\n        throw new IOException(\"Failed to obtain compression information for entry\", e);\n      }\n\n      addToEntries(next);\n    }\n  }\n\n  /**\n   * Waits until {@link #uncompressedEntries} is empty.\n   *\n   * @throws IOException the exception reported in the future computation, if any, or failed to add\n   *     a file to the archive\n   */\n  private void processAllReadyEntriesWithWait() throws IOException {\n    processAllReadyEntries();\n    while (!uncompressedEntries.isEmpty()) {\n      /*\n       * Wait for the first future to complete and then try again. Keep looping until we're\n       * done.\n       */\n      StoredEntry first = uncompressedEntries.get(0);\n      CentralDirectoryHeader cdh = first.getCentralDirectoryHeader();\n      cdh.getCompressionInfoWithWait();\n\n      processAllReadyEntries();\n    }\n  }\n\n  /**\n   * Adds a new file to {@link #entries}. This is actually added to the zip and its space allocated\n   * in the {@link #map}.\n   *\n   * @param newEntry the new entry to add\n   * @throws IOException failed to add the file\n   */\n  private void addToEntries(final StoredEntry newEntry) throws IOException {\n    Preconditions.checkArgument(\n        newEntry.getDataDescriptorType() == DataDescriptorType.NO_DATA_DESCRIPTOR,\n        \"newEntry has data descriptor\");\n\n    /*\n     * If there is a file with the same name in the archive, remove it. We remove it by\n     * calling delete() on the entry (this is the public API to remove a file from the archive).\n     * StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)}  to perform\n     * data structure cleanup.\n     */\n    FileUseMapEntry<StoredEntry> toReplace =\n        entries.get(newEntry.getCentralDirectoryHeader().getName());\n    final StoredEntry replaceStore;\n    if (toReplace != null) {\n      replaceStore = toReplace.getStore();\n      Preconditions.checkNotNull(\n          replaceStore, \"File to replace at %s is null\", toReplace.getStart());\n      replaceStore.delete(false);\n    } else {\n      replaceStore = null;\n    }\n\n    FileUseMapEntry<StoredEntry> fileUseMapEntry = positionInFile(newEntry, PositionHint.ANYWHERE);\n    entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry);\n\n    dirty = true;\n\n    notify(ext -> ext.added(newEntry, replaceStore));\n  }\n\n  /**\n   * Finds a location in the zip where this entry will be added to and create the map entry. This\n   * method cannot be called if there is already a map entry for the given entry (if you do that,\n   * then you're doing something wrong somewhere).\n   *\n   * <p>This may delete the central directory and EOCD (if it deletes one, it deletes the other) if\n   * there is no space before the central directory. Otherwise, the file would be added after the\n   * central directory. This would force a new central directory to be written when updating the\n   * file and would create a hole in the zip. Me no like holes. Holes are evil.\n   *\n   * @param entry the entry to place in the zip\n   * @param positionHint hint to where the file should be positioned\n   * @return the position in the file where the entry should be placed\n   */\n  private FileUseMapEntry<StoredEntry> positionInFile(StoredEntry entry, PositionHint positionHint)\n      throws IOException {\n    deleteDirectoryAndEocd();\n    long size = entry.getInFileSize();\n    int localHeaderSize = entry.getLocalHeaderSize();\n    int alignment = chooseAlignment(entry);\n\n    FileUseMap.PositionAlgorithm algorithm;\n\n    switch (positionHint) {\n      case LOWEST_OFFSET:\n        algorithm = FileUseMap.PositionAlgorithm.FIRST_FIT;\n        break;\n      case ANYWHERE:\n        algorithm = FileUseMap.PositionAlgorithm.BEST_FIT;\n        break;\n      default:\n        throw new AssertionError();\n    }\n\n    long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm);\n    long newEnd = newOffset + entry.getInFileSize();\n    if (newEnd > map.size()) {\n      map.extend(newEnd);\n    }\n\n    return map.add(newOffset, newEnd, entry);\n  }\n\n  /**\n   * Determines what is the alignment value of an entry.\n   *\n   * @param entry the entry\n   * @return the alignment value, {@link AlignmentRule#NO_ALIGNMENT} if there is no alignment\n   *     required for the entry\n   * @throws IOException failed to determine the alignment\n   */\n  private int chooseAlignment(StoredEntry entry) throws IOException {\n    CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader();\n    CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait();\n\n    boolean isCompressed = compressionInfo.getMethod() != CompressionMethod.STORE;\n    if (isCompressed) {\n      return AlignmentRule.NO_ALIGNMENT;\n    } else {\n      return alignmentRule.alignment(cdh.getName());\n    }\n  }\n\n  /**\n   * Adds all files from another zip file, maintaining their compression. Files specified in\n   * <em>src</em> that are already on this file will replace the ones in this file. However, if\n   * their sizes and checksums are equal, they will be ignored.\n   *\n   * <p>This method will not perform any changes in itself, it will only update in-memory data\n   * structures. To actually write the zip file, invoke either {@link #update()} or {@link\n   * #close()}.\n   *\n   * @param src the source archive\n   * @param ignoreFilter predicate that, if {@code true}, identifies files in <em>src</em> that\n   *     should be ignored by merging; merging will behave as if these files were not there\n   * @throws IOException failed to read from <em>src</em> or write on the output\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void mergeFrom(ZFile src, Predicate<String> ignoreFilter) throws IOException {\n    checkNotInReadOnlyMode();\n\n    for (StoredEntry fromEntry : src.entries()) {\n      if (ignoreFilter.apply(fromEntry.getCentralDirectoryHeader().getName())) {\n        continue;\n      }\n\n      boolean replaceCurrent = true;\n      String path = fromEntry.getCentralDirectoryHeader().getName();\n      FileUseMapEntry<StoredEntry> currentEntry = entries.get(path);\n\n      if (currentEntry != null) {\n        long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize();\n        long fromCrc = fromEntry.getCentralDirectoryHeader().getCrc32();\n\n        StoredEntry currentStore = currentEntry.getStore();\n        Preconditions.checkNotNull(currentStore, \"Entry at %s is null\", currentEntry.getStart());\n\n        long currentSize = currentStore.getCentralDirectoryHeader().getUncompressedSize();\n        long currentCrc = currentStore.getCentralDirectoryHeader().getCrc32();\n\n        if (fromSize == currentSize && fromCrc == currentCrc) {\n          replaceCurrent = false;\n        }\n      }\n\n      if (replaceCurrent) {\n        CentralDirectoryHeader fromCdr = fromEntry.getCentralDirectoryHeader();\n        CentralDirectoryHeaderCompressInfo fromCompressInfo = fromCdr.getCompressionInfoWithWait();\n        CentralDirectoryHeader newFileData;\n        try {\n          /*\n           * We make two changes in the central directory from the file to merge:\n           * we reset the offset to force the entry to be written and we reset the\n           * deferred CRC bit as we don't need the extra stuff after the file. It takes\n           * space and is totally useless.\n           */\n          newFileData = fromCdr.clone();\n          newFileData.setOffset(-1);\n          newFileData.resetDeferredCrc();\n        } catch (CloneNotSupportedException e) {\n          throw new IOException(\"Failed to clone CDR.\", e);\n        }\n\n        /*\n         * Read the data (read directly the compressed source if there is one).\n         */\n        ProcessedAndRawByteSources fromSource = fromEntry.getSource();\n        InputStream fromInput = fromSource.getRawByteSource().openStream();\n        long sourceSize = fromSource.getRawByteSource().size();\n        if (sourceSize > Integer.MAX_VALUE) {\n          throw new IOException(\"Cannot read source with \" + sourceSize + \" bytes.\");\n        }\n\n        byte[] data = new byte[Ints.checkedCast(sourceSize)];\n        int read = 0;\n        while (read < data.length) {\n          int r = fromInput.read(data, read, data.length - read);\n          Verify.verify(r >= 0, \"There should be at least 'size' bytes in the stream.\");\n          read += r;\n        }\n\n        /*\n         * Build the new source and wrap it around an inflater source if data came from\n         * a compressed source.\n         */\n        CloseableByteSource rawContents = storage.fromSource(fromSource.getRawByteSource());\n        CloseableByteSource processedContents;\n        if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) {\n          //noinspection IOResourceOpenedButNotSafelyClosed\n          processedContents = new InflaterByteSource(rawContents);\n        } else {\n          processedContents = rawContents;\n        }\n\n        ProcessedAndRawByteSources newSource =\n            new ProcessedAndRawByteSources(processedContents, rawContents);\n\n        /*\n         * Add will replace any current entry with the same name.\n         */\n        StoredEntry newEntry = new StoredEntry(newFileData, this, newSource, storage);\n        add(newEntry);\n      }\n    }\n  }\n\n  /**\n   * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()} or\n   * {@link #close()} are invoked.\n   *\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void touch() {\n    checkNotInReadOnlyMode();\n    dirty = true;\n  }\n\n  /**\n   * Wait for any background tasks to finish and report any errors. In general this method does not\n   * need to be invoked directly as errors from background tasks are reported during {@link\n   * #add(String, InputStream, boolean)}, {@link #update()} and {@link #close()}. However, if\n   * required for some purposes, <em>e.g.</em>, ensuring all notifications have been done to\n   * extensions, then this method may be called. It will wait for all background tasks to complete.\n   *\n   * @throws IOException some background work failed\n   */\n  public void finishAllBackgroundTasks() throws IOException {\n    processAllReadyEntriesWithWait();\n  }\n\n  /**\n   * Realigns all entries in the zip. This is equivalent to call {@link StoredEntry#realign()} for\n   * all entries in the zip file.\n   *\n   * @return has any entry been changed? Note that for entries that have not yet been written on the\n   *     file, realignment does not count as a change as nothing needs to be updated in the file;\n   *     entries that have been updated may have been recreated and the existing references outside\n   *     of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid\n   * @throws IOException failed to realign the zip; some entries in the zip may have been lost due\n   *     to the I/O error\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public boolean realign() throws IOException {\n    checkNotInReadOnlyMode();\n\n    boolean anyChanges = false;\n    for (StoredEntry entry : entries()) {\n      anyChanges |= entry.realign();\n    }\n\n    if (anyChanges) {\n      dirty = true;\n    }\n\n    return anyChanges;\n  }\n\n  /**\n   * Realigns a stored entry, if necessary. Realignment is done by removing and re-adding the file\n   * if it was not aligned.\n   *\n   * @param entry the entry to realign\n   * @return has the entry been changed? Note that if the entry has not yet been written on the\n   *     file, realignment does not count as a change as nothing needs to be updated in the file\n   * @throws IOException failed to read/write an entry; the entry may no longer exist in the file\n   */\n  boolean realign(StoredEntry entry) throws IOException {\n    FileUseMapEntry<StoredEntry> mapEntry =\n        entries.get(entry.getCentralDirectoryHeader().getName());\n    Verify.verify(entry == mapEntry.getStore());\n    long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize();\n\n    int expectedAlignment = chooseAlignment(entry);\n    long misalignment = currentDataOffset % expectedAlignment;\n    if (misalignment == 0) {\n      /*\n       * Good. File is aligned properly.\n       */\n      return false;\n    }\n\n    if (entry.getCentralDirectoryHeader().getOffset() == -1) {\n      /*\n       * File is not aligned but it is not written. We do not really need to do much other\n       * than find another place in the map.\n       */\n      map.remove(mapEntry);\n      long newStart =\n          map.locateFree(\n              mapEntry.getSize(),\n              entry.getLocalHeaderSize(),\n              expectedAlignment,\n              FileUseMap.PositionAlgorithm.BEST_FIT);\n      mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry);\n      entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);\n\n      /*\n       * Just for safety. We're modifying the in-memory structures but the file should\n       * already be marked as dirty.\n       */\n      Verify.verify(dirty);\n\n      return false;\n    }\n\n    /*\n     * Get the entry data source, but check if we have a compressed one (we don't want to\n     * inflate and deflate).\n     */\n    CentralDirectoryHeaderCompressInfo compressInfo =\n        entry.getCentralDirectoryHeader().getCompressionInfoWithWait();\n\n    ProcessedAndRawByteSources source = entry.getSource();\n\n    CentralDirectoryHeader clonedCdh;\n    try {\n      clonedCdh = entry.getCentralDirectoryHeader().clone();\n    } catch (CloneNotSupportedException e) {\n      Verify.verify(false);\n      return false;\n    }\n\n    /*\n     * We make two changes in the central directory when realigning:\n     * we reset the offset to force the entry to be written and we reset the\n     * deferred CRC bit as we don't need the extra stuff after the file. It takes\n     * space and is totally useless and we may need the extra space to realign the entry...\n     */\n    clonedCdh.setOffset(-1);\n    clonedCdh.resetDeferredCrc();\n\n    CloseableByteSource rawContents = storage.fromSource(source.getRawByteSource());\n    CloseableByteSource processedContents;\n\n    if (compressInfo.getMethod() == CompressionMethod.DEFLATE) {\n      //noinspection IOResourceOpenedButNotSafelyClosed\n      processedContents = new InflaterByteSource(rawContents);\n    } else {\n      processedContents = rawContents;\n    }\n\n    ProcessedAndRawByteSources newSource =\n        new ProcessedAndRawByteSources(processedContents, rawContents);\n\n    /*\n     * Add the new file. This will replace the existing one.\n     */\n    StoredEntry newEntry = new StoredEntry(clonedCdh, this, newSource, storage);\n    add(newEntry);\n    return true;\n  }\n\n  /**\n   * Adds an extension to this zip file.\n   *\n   * @param extension the listener to add\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void addZFileExtension(ZFileExtension extension) {\n    checkNotInReadOnlyMode();\n    extensions.add(extension);\n  }\n\n  /**\n   * Removes an extension from this zip file.\n   *\n   * @param extension the listener to remove\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void removeZFileExtension(ZFileExtension extension) {\n    checkNotInReadOnlyMode();\n    extensions.remove(extension);\n  }\n\n  /**\n   * Notifies all extensions, collecting their execution requests and running them.\n   *\n   * @param function the function to apply to all listeners, it will generally invoke the\n   *     notification method on the listener and return the result of that invocation\n   * @throws IOException failed to process some extensions\n   */\n  private void notify(IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function)\n      throws IOException {\n    for (ZFileExtension fl : Lists.newArrayList(extensions)) {\n      IOExceptionRunnable r = function.apply(fl);\n      if (r != null) {\n        toRun.add(r);\n      }\n    }\n\n    if (!isNotifying) {\n      isNotifying = true;\n\n      try {\n        while (!toRun.isEmpty()) {\n          IOExceptionRunnable r = toRun.remove(0);\n          r.run();\n        }\n      } finally {\n        isNotifying = false;\n      }\n    }\n  }\n\n  /**\n   * Directly writes data in the zip file. <strong>Incorrect use of this method may corrupt the zip\n   * file</strong>. Invoking this method may force the zip to be reopened in read/write mode.\n   *\n   * @param offset the offset at which data should be written\n   * @param data the data to write, may be an empty array\n   * @param start start offset in {@code data} where data to write is located\n   * @param count number of bytes of data to write\n   * @throws IOException failed to write the data\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void directWrite(long offset, byte[] data, int start, int count) throws IOException {\n    checkNotInReadOnlyMode();\n\n    Preconditions.checkArgument(offset >= 0, \"offset < 0\");\n    Preconditions.checkArgument(start >= 0, \"start >= 0\");\n    Preconditions.checkArgument(count >= 0, \"count >= 0\");\n\n    if (data.length == 0) {\n      return;\n    }\n\n    Preconditions.checkArgument(start <= data.length, \"start > data.length\");\n    Preconditions.checkArgument(start + count <= data.length, \"start + count > data.length\");\n\n    reopenRw();\n    Preconditions.checkNotNull(raf, \"raf == null\");\n\n    raf.seek(offset);\n    raf.write(data, start, count);\n  }\n\n  /**\n   * Same as {@code directWrite(offset, data, 0, data.length)}.\n   *\n   * @param offset the offset at which data should be written\n   * @param data the data to write, may be an empty array\n   * @throws IOException failed to write the data\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void directWrite(long offset, byte[] data) throws IOException {\n    directWrite(offset, data, 0, data.length);\n  }\n\n  /**\n   * Returns the current size (in bytes) of the underlying file.\n   *\n   * @throws IOException if an I/O error occurs\n   */\n  public long directSize() throws IOException {\n    /*\n     * Only force a reopen if the file is closed.\n     */\n    if (raf == null) {\n      reopenRw();\n      Preconditions.checkNotNull(raf, \"raf == null\");\n    }\n    return raf.length();\n  }\n\n  /**\n   * Directly reads data from the zip file. Invoking this method may force the zip to be reopened in\n   * read/write mode.\n   *\n   * @param offset the offset at which data should be written\n   * @param data the array where read data should be stored\n   * @param start start position in the array where to write data to\n   * @param count how many bytes of data can be written\n   * @return how many bytes of data have been written or {@code -1} if there are no more bytes to be\n   *     read\n   * @throws IOException failed to write the data\n   */\n  public int directRead(long offset, byte[] data, int start, int count) throws IOException {\n    Preconditions.checkArgument(start >= 0, \"start >= 0\");\n    Preconditions.checkArgument(count >= 0, \"count >= 0\");\n    Preconditions.checkArgument(start <= data.length, \"start > data.length\");\n    Preconditions.checkArgument(start + count <= data.length, \"start + count > data.length\");\n    return directRead(offset, ByteBuffer.wrap(data, start, count));\n  }\n\n  /**\n   * Directly reads data from the zip file. Invoking this method may force the zip to be reopened in\n   * read/write mode.\n   *\n   * @param offset the offset from which data should be read\n   * @param dest the output buffer to fill with data from the {@code offset}.\n   * @return how many bytes of data have been written or {@code -1} if there are no more bytes to be\n   *     read\n   * @throws IOException failed to write the data\n   */\n  public int directRead(long offset, ByteBuffer dest) throws IOException {\n    Preconditions.checkArgument(offset >= 0, \"offset < 0\");\n\n    if (!dest.hasRemaining()) {\n      return 0;\n    }\n\n    /*\n     * Only force a reopen if the file is closed.\n     */\n    if (raf == null) {\n      reopenRw();\n      Preconditions.checkNotNull(raf, \"raf == null\");\n    }\n\n    raf.seek(offset);\n    return raf.getChannel().read(dest);\n  }\n\n  /**\n   * Same as {@code directRead(offset, data, 0, data.length)}.\n   *\n   * @param offset the offset at which data should be read\n   * @param data receives the read data, may be an empty array\n   * @throws IOException failed to read the data\n   */\n  public int directRead(long offset, byte[] data) throws IOException {\n    return directRead(offset, data, 0, data.length);\n  }\n\n  /**\n   * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all the\n   * requested data.\n   *\n   * @param offset the offset at which to start reading\n   * @param data the array that receives the data read\n   * @throws IOException failed to read some data or there is not enough data to read\n   */\n  public void directFullyRead(long offset, byte[] data) throws IOException {\n    directFullyRead(offset, ByteBuffer.wrap(data));\n  }\n\n  /**\n   * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read\n   * all the requested data.\n   *\n   * @param offset the offset at which to start reading\n   * @param dest the output buffer to fill with data\n   * @throws IOException failed to read some data or there is not enough data to read\n   */\n  public void directFullyRead(long offset, ByteBuffer dest) throws IOException {\n    Preconditions.checkArgument(offset >= 0, \"offset < 0\");\n\n    if (!dest.hasRemaining()) {\n      return;\n    }\n\n    /*\n     * Only force a reopen if the file is closed.\n     */\n    if (raf == null) {\n      reopenRw();\n      Preconditions.checkNotNull(raf, \"raf == null\");\n    }\n\n    FileChannel fileChannel = raf.getChannel();\n    while (dest.hasRemaining()) {\n      fileChannel.position(offset);\n      int chunkSize = fileChannel.read(dest);\n      if (chunkSize == -1) {\n        throw new EOFException(\"Failed to read \" + dest.remaining() + \" more bytes: premature EOF\");\n      }\n      offset += chunkSize;\n    }\n  }\n\n  /**\n   * Adds all files and directories recursively.\n   *\n   * <p>Equivalent to calling {@link #addAllRecursively(File, Predicate)} using a predicate that\n   * always returns {@code true}\n   *\n   * @param file a file or directory; if it is a directory, all files and directories will be added\n   *     recursively\n   * @throws IOException failed to some (or all ) of the files\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void addAllRecursively(File file) throws IOException {\n    checkNotInReadOnlyMode();\n    addAllRecursively(file, f -> true);\n  }\n\n  /**\n   * Adds all files and directories recursively.\n   *\n   * @param file a file or directory; if it is a directory, all files and directories will be added\n   *     recursively\n   * @param mayCompress a function that decides whether files may be compressed\n   * @throws IOException failed to some (or all ) of the files\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  public void addAllRecursively(File file, Predicate<? super File> mayCompress) throws IOException {\n    checkNotInReadOnlyMode();\n\n    addAllRecursively(file, file, mayCompress);\n  }\n\n  /**\n   * Adds all files and directories recursively.\n   *\n   * @param file a file or directory; if it is a directory, all files and directories will be added\n   *     recursively\n   * @param base the file/directory to compute the relative path from\n   * @param mayCompress a function that decides whether files may be compressed\n   * @throws IOException failed to some (or all ) of the files\n   * @throws IllegalStateException if the file is in read-only mode\n   */\n  private void addAllRecursively(File file, File base, Predicate<? super File> mayCompress)\n      throws IOException {\n    // If we're just adding a file, do not compute a relative path, but rather use the file name\n    // as path.\n    String path =\n        Objects.equal(file, base)\n            ? file.getName()\n            : base.toURI().relativize(file.toURI()).getPath();\n\n    /*\n     * The case of file.isFile() is different because if file.isFile() we will add it to the\n     * zip in the root. However, if file.isDirectory() we won't add it and add its children.\n     */\n    if (file.isFile()) {\n      boolean mayCompressFile = mayCompress.apply(file);\n\n      try (Closer closer = Closer.create()) {\n        FileInputStream fileInput = closer.register(new FileInputStream(file));\n        add(path, fileInput, mayCompressFile);\n      }\n\n      return;\n    } else if (file.isDirectory()) {\n      // Add an entry for the directory, unless it is the base.\n      if (!file.equals(base)) {\n        try (Closer closer = Closer.create()) {\n          InputStream stream = closer.register(new ByteArrayInputStream(new byte[0]));\n          add(path, stream, false);\n        }\n      }\n\n      // Add recursively.\n      File[] directoryContents = file.listFiles();\n      if (directoryContents != null) {\n        Arrays.sort(directoryContents, (f0, f1) -> f0.getName().compareTo(f1.getName()));\n        for (File subFile : directoryContents) {\n          addAllRecursively(subFile, base, mayCompress);\n        }\n      }\n    }\n  }\n\n  /**\n   * Obtains the offset at which the central directory exists, or at which it will be written if the\n   * zip file were to be flushed immediately.\n   *\n   * @return the offset, in bytes, where the central directory is or will be written; this value\n   *     includes any extra offset for the central directory\n   */\n  public long getCentralDirectoryOffset() {\n    if (directoryEntry != null) {\n      return directoryEntry.getStart();\n    }\n\n    /*\n     * If there are no entries, the central directory is written at the start of the file.\n     */\n    if (entries.isEmpty()) {\n      return extraDirectoryOffset;\n    }\n\n    /*\n     * The Central Directory is written after all entries. This will be at the end of the file\n     * if the\n     */\n    return map.usedSize() + extraDirectoryOffset;\n  }\n\n  /**\n   * Obtains the size of the central directory, if the central directory is written in the zip file.\n   *\n   * @return the size of the central directory or {@code -1} if the central directory has not been\n   *     computed\n   */\n  public long getCentralDirectorySize() {\n    if (directoryEntry != null) {\n      return directoryEntry.getSize();\n    }\n\n    if (entries.isEmpty()) {\n      return 0;\n    }\n\n    return 1;\n  }\n\n  /**\n   * Obtains the offset of the EOCD record, if the EOCD has been written to the file.\n   *\n   * @return the offset of the EOCD or {@code -1} if none exists yet\n   */\n  public long getEocdOffset() {\n    if (eocdEntry == null) {\n      return -1;\n    }\n\n    return eocdEntry.getStart();\n  }\n\n  /**\n   * Obtains the size of the EOCD record, if the EOCD has been written to the file.\n   *\n   * @return the size of the EOCD of {@code -1} it none exists yet\n   */\n  public long getEocdSize() {\n    if (eocdEntry == null) {\n      return -1;\n    }\n\n    return eocdEntry.getSize();\n  }\n\n  /**\n   * Obtains the comment in the EOCD.\n   *\n   * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done\n   */\n  public byte[] getEocdComment() {\n    if (eocdEntry == null) {\n      Verify.verify(eocdComment != null);\n      byte[] eocdCommentCopy = new byte[eocdComment.length];\n      System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length);\n      return eocdCommentCopy;\n    }\n\n    Eocd eocd = eocdEntry.getStore();\n    Verify.verify(eocd != null);\n    return eocd.getComment();\n  }\n\n  /**\n   * Sets the comment in the EOCD.\n   *\n   * @param comment the new comment; no conversion is done, these exact bytes will be placed in the\n   *     EOCD comment\n   * @throws IllegalStateException if file is in read-only mode\n   */\n  public void setEocdComment(byte[] comment) {\n    checkNotInReadOnlyMode();\n\n    if (comment.length > MAX_EOCD_COMMENT_SIZE) {\n      throw new IllegalArgumentException(\n          \"EOCD comment size (\"\n              + comment.length\n              + \") is larger than the maximum allowed (\"\n              + MAX_EOCD_COMMENT_SIZE\n              + \")\");\n    }\n\n    // Check if the EOCD signature appears anywhere in the comment we need to check if it\n    // is valid.\n    for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) {\n      // Remember: little endian...\n      if (comment[i] == EOCD_SIGNATURE[3]\n          && comment[i + 1] == EOCD_SIGNATURE[2]\n          && comment[i + 2] == EOCD_SIGNATURE[1]\n          && comment[i + 3] == EOCD_SIGNATURE[0]) {\n        // We found a possible EOCD signature at position i. Try to read it.\n        ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i);\n        try {\n          new Eocd(bytes);\n          throw new IllegalArgumentException(\n              \"Position \" + i + \" of the comment contains a valid EOCD record.\");\n        } catch (IOException e) {\n          // Fine, this is an invalid record. Move along...\n        }\n      }\n    }\n\n    deleteDirectoryAndEocd();\n    eocdComment = new byte[comment.length];\n    System.arraycopy(comment, 0, eocdComment, 0, comment.length);\n    dirty = true;\n  }\n\n  /**\n   * Sets an extra offset for the central directory. See class description for details. Changing\n   * this value will mark the file as dirty and force a rewrite of the central directory when\n   * updated.\n   *\n   * @param offset the offset or {@code 0} to write the central directory at its current location\n   * @throws IllegalStateException if file is in read-only mode\n   */\n  public void setExtraDirectoryOffset(long offset) {\n    checkNotInReadOnlyMode();\n    Preconditions.checkArgument(offset >= 0, \"offset < 0\");\n\n    if (extraDirectoryOffset != offset) {\n      extraDirectoryOffset = offset;\n      deleteDirectoryAndEocd();\n      dirty = true;\n    }\n  }\n\n  /**\n   * Obtains the extra offset for the central directory. See class description for details.\n   *\n   * @return the offset or {@code 0} if no offset is set\n   */\n  public long getExtraDirectoryOffset() {\n    return extraDirectoryOffset;\n  }\n\n  /**\n   * Obtains whether this {@code ZFile} is ignoring timestamps.\n   *\n   * @return are the timestamps being ignored?\n   */\n  public boolean areTimestampsIgnored() {\n    return noTimestamps;\n  }\n\n  /**\n   * Sorts all files in the zip. This will force all files to be loaded and will wait for all\n   * background tasks to complete. Sorting files is never done implicitly and will operate in memory\n   * only (maybe reading files from the zip disk into memory, if needed). It will leave the zip in\n   * dirty state, requiring a call to {@link #update()} to force the entries to be written to disk.\n   *\n   * @throws IOException failed to load or move a file in the zip\n   * @throws IllegalStateException if file is in read-only mode\n   */\n  public void sortZipContents() throws IOException {\n    checkNotInReadOnlyMode();\n    reopenRw();\n\n    processAllReadyEntriesWithWait();\n\n    Verify.verify(uncompressedEntries.isEmpty());\n\n    SortedSet<StoredEntry> sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME);\n    for (FileUseMapEntry<StoredEntry> fmEntry : entries.values()) {\n      StoredEntry entry = fmEntry.getStore();\n      Preconditions.checkNotNull(entry);\n      sortedEntries.add(entry);\n      entry.loadSourceIntoMemory();\n\n      map.remove(fmEntry);\n    }\n\n    entries.clear();\n    for (StoredEntry entry : sortedEntries) {\n      String name = entry.getCentralDirectoryHeader().getName();\n      FileUseMapEntry<StoredEntry> positioned = positionInFile(entry, PositionHint.LOWEST_OFFSET);\n\n      entries.put(name, positioned);\n    }\n\n    dirty = true;\n  }\n\n  /**\n   * Obtains the filesystem path to the zip file.\n   *\n   * @return the file that may or may not exist (depending on whether something existed there before\n   *     the zip was created and on whether the zip has been updated or not)\n   */\n  public File getFile() {\n    return file;\n  }\n\n  public DataSource asDataSource() throws IOException {\n    if (raf == null) {\n      reopenRw();\n      Preconditions.checkNotNull(raf, \"raf == null\");\n    }\n    return DataSources.asDataSource(this.raf);\n  }\n\n  public DataSource asDataSource(long offset, long size) throws IOException {\n    if (raf == null) {\n      reopenRw();\n      Preconditions.checkNotNull(raf, \"raf == null\");\n    }\n    return DataSources.asDataSource(this.raf, offset, size);\n  }\n\n  /**\n   * Creates a new verify log.\n   *\n   * @return the new verify log\n   */\n  VerifyLog makeVerifyLog() {\n    VerifyLog log = verifyLogFactory.get();\n    Preconditions.checkNotNull(log, \"log == null\");\n    return log;\n  }\n\n  /**\n   * Obtains the zip file's verify log.\n   *\n   * @return the verify log\n   */\n  VerifyLog getVerifyLog() {\n    return verifyLog;\n  }\n\n  /**\n   * Are there in-memory changes that have not been written to the zip file?\n   *\n   * <p>Waits for all pending processing which may make changes.\n   */\n  public boolean hasPendingChangesWithWait() throws IOException {\n    processAllReadyEntriesWithWait();\n    return dirty;\n  }\n\n  /**\n   * Obtains the storage used by the zip to store data.\n   *\n   * @return the storage object that should only be used to query data; using this storage for any\n   *     purposes other than statistics may have undefined results\n   */\n  public ByteStorage getStorage() {\n    return storage;\n  }\n\n  /** Hint to where files should be positioned. */\n  enum PositionHint {\n    /** File may be positioned anywhere, caller doesn't care. */\n    ANYWHERE,\n\n    /** File should be positioned at the lowest offset possible. */\n    LOWEST_OFFSET\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZFileExtension.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.utils.IOExceptionRunnable;\nimport java.io.IOException;\nimport javax.annotation.Nullable;\n\n/**\n * An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and\n * when files are added or removed from the zip. These notifications are received after the zip has\n * been updated in memory for open, when files are added or removed and when the zip has been\n * updated on disk or closed.\n *\n * <p>An extension is also notified before the file is updated, allowing it to modify the file\n * before the update happens. If it does, then all extensions are notified of the changes on the zip\n * file. Because the order of the notifications is preserved, all extensions are notified in the\n * same order. For example, if two extensions E1 and E2 are registered and they both add a file at\n * update time, this would be the flow:\n *\n * <ul>\n *   <li>E1 receives {@code beforeUpdate} notification.\n *   <li>E1 adds file F1 to the zip (notifying the addition is suspended because another\n *       notification is in progress).\n *   <li>E2 receives {@code beforeUpdate} notification.\n *   <li>E2 adds file F2 to the zip (notifying the addition is suspended because another\n *       notification is in progress).\n *   <li>E1 is notified F1 was added.\n *   <li>E2 is notified F1 was added.\n *   <li>E1 is notified F2 was added.\n *   <li>E2 is notified F2 was added.\n *   <li>(zip file is updated on disk)\n *   <li>E1 is notified the zip was updated.\n *   <li>E2 is notified the zip was updated.\n * </ul>\n *\n * <p>An extension should not modify the zip file when notified of changes. If allowed, this would\n * break event notification order in case multiple extensions are registered with the zip file. To\n * allow performing changes to the zip file, all notification method return a {@code\n * IOExceptionRunnable} that is invoked when {@link ZFile} has finished notifying all extensions.\n */\npublic abstract class ZFileExtension {\n\n  /**\n   * The zip file has been open and the zip's contents have been read. The default implementation\n   * does nothing and returns {@code null}.\n   *\n   * @return an optional runnable to run when notification of all listeners has ended\n   * @throws IOException failed to process the event\n   */\n  @Nullable\n  public IOExceptionRunnable open() throws IOException {\n    return null;\n  }\n\n  /**\n   * The zip will be updated. This method allows the extension to register changes to the zip file\n   * before the file is written. The default implementation does nothing and returns {@code null}.\n   *\n   * <p>After this notification is received, the extension will receive further {@link\n   * #added(StoredEntry, StoredEntry)} and {@link #removed(StoredEntry)} notifications if it or\n   * other extensions add or remove files before update.\n   *\n   * <p>When no more files are updated, the {@link #entriesWritten()} notification is sent.\n   *\n   * @return an optional runnable to run when notification of all listeners has ended\n   * @throws IOException failed to process the event\n   */\n  @Nullable\n  public IOExceptionRunnable beforeUpdate() throws IOException {\n    return null;\n  }\n\n  /**\n   * This notification is sent when all entries have been written in the file but the central\n   * directory and the EOCD have not yet been written. No entries should be added, removed or\n   * updated during this notification. If this method forces an update of either the central\n   * directory or EOCD, then this method will be invoked again for all extensions with the new\n   * central directory and EOCD.\n   *\n   * <p>After this notification, {@link #updated()} is sent.\n   *\n   * @throws IOException failed to process the event\n   */\n  public void entriesWritten() throws IOException {}\n\n  /**\n   * The zip file has been updated on disk. The default implementation does nothing.\n   *\n   * @throws IOException failed to perform update tasks\n   */\n  public void updated() throws IOException {}\n\n  /**\n   * The zip file has been closed. Note that if {@link ZFile#close()} requires that the zip file be\n   * updated (because it had in-memory changes), {@link #updated()} will be called before this\n   * method. The default implementation does nothing.\n   */\n  public void closed() {}\n\n  /**\n   * A new entry has been added to the zip, possibly replacing an entry in there. The default\n   * implementation does nothing and returns {@code null}.\n   *\n   * @param entry the entry that was added\n   * @param replaced the entry that was replaced, if any\n   * @return an optional runnable to run when notification of all listeners has ended\n   */\n  @Nullable\n  public IOExceptionRunnable added(StoredEntry entry, @Nullable StoredEntry replaced) {\n    return null;\n  }\n\n  /**\n   * An entry has been removed from the zip. This method is not invoked for entries that have been\n   * replaced. Those entries are notified using <em>replaced</em> in {@link #added(StoredEntry,\n   * StoredEntry)}. The default implementation does nothing and returns {@code null}.\n   *\n   * @param entry the entry that was deleted\n   * @return an optional runnable to run when notification of all listeners has ended\n   */\n  @Nullable\n  public IOExceptionRunnable removed(StoredEntry entry) {\n    return null;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZFileOptions.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorageFactory;\nimport com.android.tools.build.apkzlib.bytestorage.ChunkBasedByteStorageFactory;\nimport com.android.tools.build.apkzlib.bytestorage.OverflowToDiskByteStorageFactory;\nimport com.android.tools.build.apkzlib.bytestorage.TemporaryDirectory;\nimport com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor;\nimport com.android.tools.build.apkzlib.zip.utils.ByteTracker;\nimport com.google.common.base.Supplier;\nimport java.util.zip.Deflater;\n\n/** Options to create a {@link ZFile}. */\npublic class ZFileOptions {\n\n  /** The storage to use. */\n  private ByteStorageFactory storageFactory;\n\n  /** The compressor to use. */\n  private Compressor compressor;\n\n  /** Should timestamps be zeroed? */\n  private boolean noTimestamps;\n\n  /** The alignment rule to use. */\n  private AlignmentRule alignmentRule;\n\n  /** Should the extra field be used to cover empty space? */\n  private boolean coverEmptySpaceUsingExtraField;\n\n  /** Should files be automatically sorted before update? */\n  private boolean autoSortFiles;\n\n  /**\n   * Skip expensive validation during {@link ZFile} creation?\n   *\n   * <p>During incremental build we are absolutely sure that the zip file is valid, so we do not\n   * have to spend time verifying different fields (some of these checks are relatively expensive\n   * and should be skipped if possible for performance)\n   */\n  private boolean skipValidation;\n\n  /** Factory creating verification logs to use. */\n  private Supplier<VerifyLog> verifyLogFactory;\n\n  /**\n   * Whether to always generate the MANIFEST.MF file regardless whether the APK will be signed with\n   * v1 signing scheme (i.e. jar signing).\n   */\n  private boolean alwaysGenerateJarManifest;\n\n  /** Creates a new options object. All options are set to their defaults. */\n  public ZFileOptions() {\n    storageFactory =\n        new ChunkBasedByteStorageFactory(\n            new OverflowToDiskByteStorageFactory(TemporaryDirectory::newSystemTemporaryDirectory));\n    compressor = new DeflateExecutionCompressor(Runnable::run, Deflater.DEFAULT_COMPRESSION);\n    alignmentRule = AlignmentRules.compose();\n    verifyLogFactory = VerifyLogs::devNull;\n\n    // We set this to true because many utilities stream the zip and expect no space between entries\n    // in the zip file.\n    coverEmptySpaceUsingExtraField = true;\n    skipValidation = false;\n    // True by default for backwards compatibility.\n    alwaysGenerateJarManifest = true;\n  }\n\n  /**\n   * Obtains the ZFile's byte storage factory.\n   *\n   * @return the factory used to create byte storages used to store data\n   */\n  public ByteStorageFactory getStorageFactory() {\n    return storageFactory;\n  }\n\n  @Deprecated\n  public ByteTracker getTracker() {\n    return new ByteTracker();\n  }\n\n  /**\n   * Sets the byte storage factory to use.\n   *\n   * @param storage the factory to use to create storage for new instances of {@link ZFile} created\n   *     for these options.\n   */\n  public ZFileOptions setStorageFactory(ByteStorageFactory storage) {\n    this.storageFactory = storage;\n    return this;\n  }\n\n  /**\n   * Obtains the compressor to use.\n   *\n   * @return the compressor\n   */\n  public Compressor getCompressor() {\n    return compressor;\n  }\n\n  /**\n   * Sets the compressor to use.\n   *\n   * @param compressor the compressor\n   */\n  public ZFileOptions setCompressor(Compressor compressor) {\n    this.compressor = compressor;\n    return this;\n  }\n\n  /**\n   * Obtains whether timestamps should be zeroed.\n   *\n   * @return should timestamps be zeroed?\n   */\n  public boolean getNoTimestamps() {\n    return noTimestamps;\n  }\n\n  /**\n   * Sets whether timestamps should be zeroed.\n   *\n   * @param noTimestamps should timestamps be zeroed?\n   */\n  public ZFileOptions setNoTimestamps(boolean noTimestamps) {\n    this.noTimestamps = noTimestamps;\n    return this;\n  }\n\n  /**\n   * Obtains the alignment rule.\n   *\n   * @return the alignment rule\n   */\n  public AlignmentRule getAlignmentRule() {\n    return alignmentRule;\n  }\n\n  /**\n   * Sets the alignment rule.\n   *\n   * @param alignmentRule the alignment rule\n   */\n  public ZFileOptions setAlignmentRule(AlignmentRule alignmentRule) {\n    this.alignmentRule = alignmentRule;\n    return this;\n  }\n\n  /**\n   * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for an\n   * explanation on using the extra field for covering empty spaces.\n   *\n   * @return should the extra field be used to cover empty spaces?\n   */\n  public boolean getCoverEmptySpaceUsingExtraField() {\n    return coverEmptySpaceUsingExtraField;\n  }\n\n  /**\n   * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an\n   * explanation on using the extra field for covering empty spaces.\n   *\n   * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?\n   */\n  public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {\n    this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;\n    return this;\n  }\n\n  /**\n   * Obtains whether files should be automatically sorted before updating the zip file. See {@link\n   * ZFile} for an explanation on automatic sorting.\n   *\n   * @return should the file be automatically sorted?\n   */\n  public boolean getAutoSortFiles() {\n    return autoSortFiles;\n  }\n\n  /**\n   * Sets whether files should be automatically sorted before updating the zip file. See {@link\n   * ZFile} for an explanation on automatic sorting.\n   *\n   * @param autoSortFiles should the file be automatically sorted?\n   */\n  public ZFileOptions setAutoSortFiles(boolean autoSortFiles) {\n    this.autoSortFiles = autoSortFiles;\n    return this;\n  }\n\n  /**\n   * Sets the verification log factory.\n   *\n   * @param verifyLogFactory verification log factory\n   */\n  public ZFileOptions setVerifyLogFactory(Supplier<VerifyLog> verifyLogFactory) {\n    this.verifyLogFactory = verifyLogFactory;\n    return this;\n  }\n\n  /**\n   * Obtains the verification log factory. By default, the verification log doesn't store anything\n   * and will always return an empty log.\n   *\n   * @return the verification log factory\n   */\n  public Supplier<VerifyLog> getVerifyLogFactory() {\n    return verifyLogFactory;\n  }\n\n  /**\n   * Sets whether expensive validation should be skipped during {@link ZFile} creation\n   *\n   * @param skipValidation during creation?\n   */\n  public ZFileOptions setSkipValidation(boolean skipValidation) {\n    this.skipValidation = skipValidation;\n    return this;\n  }\n\n  /**\n   * Gets whether expensive validation should be performed during {@link ZFile} creation\n   *\n   * @return skip verification during creation?\n   */\n  public boolean getSkipValidation() {\n    return skipValidation;\n  }\n\n  /**\n   * Sets whether to always generate the MANIFEST.MF file, regardless whether the APK is signed with\n   * v1 signing scheme.\n   */\n  public ZFileOptions setAlwaysGenerateJarManifest(boolean alwaysGenerateJarManifest) {\n    this.alwaysGenerateJarManifest = alwaysGenerateJarManifest;\n    return this;\n  }\n\n  /** Returns whether the MANIFEST.MF file should always be generated. */\n  public boolean getAlwaysGenerateJarManifest() {\n    return alwaysGenerateJarManifest;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Zip64Eocd.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.utils.CachedSupplier;\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.google.common.primitives.Ints;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * Zip64 End of Central Directory record in a zip file.\n */\npublic class Zip64Eocd {\n\n  /**\n   * Default \"version made by\" field: upper byte needs to be 0 to set to MS-DOS compatibility. Lower\n   * byte can be anything, really. We use 0x18 because aapt uses 0x17 :)\n   */\n  private static final int DEFAULT_VERSION_MADE_BY = 0x0018;\n\n  /**\n   * Minimum size that can be stored in the {@link #F_EOCD_SIZE} field of the record.\n   */\n  private static final int MIN_EOCD_SIZE = 44;\n\n  /** Field in the record: the record signature, fixed at this value by the specification */\n  private static final ZipField.F4 F_SIGNATURE =\n      new ZipField.F4(0, 0x06064b50, \"Zip64 EOCD signature\");\n\n  /**\n   * Field in the record: the size of the central directory record, not including the first 12\n   * bytes of data (the signature and this size information). Therefore this variable should be:\n   *\n   * <code>size = sizeOfFixedFields + sizeOfVariableData - 12</code>\n   *\n   * as specified by the zip specification.\n   */\n  private static final ZipField.F8 F_EOCD_SIZE =\n      new ZipField.F8(\n          F_SIGNATURE.endOffset(),\n          \"Zip64 EOCD size\",\n          new ZipFieldInvariantMinValue(MIN_EOCD_SIZE));\n\n  /** Field in the record: ID program that made the zip (we don't actually use this). */\n  private static final ZipField.F2 F_MADE_BY =\n      new ZipField.F2(F_EOCD_SIZE.endOffset(), \"Made by\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the record: Version needed to extract the Zip. We expect this value to be at least\n   * {@link CentralDirectoryHeaderCompressInfo#VERSION_WITH_ZIP64_EXTENSIONS}. This value also\n   * determines whether we are using Version 1 or Version 2 of the Zip64 EOCD record.\n   */\n  private static final ZipField.F2 F_VERSION_EXTRACT =\n      new ZipField.F2(\n          F_MADE_BY.endOffset(),\n          \"Version to extract\",\n          new ZipFieldInvariantMinValue(\n              CentralDirectoryHeaderCompressInfo.VERSION_WITH_ZIP64_EXTENSIONS));\n\n  /**\n   * Field in the record: the number of disk where the Zip64 EOCD is located. It must be zero\n   * as multi-file archives are not supported.\n   */\n  private static final ZipField.F4 F_NUMBER_OF_DISK =\n      new ZipField.F4(F_VERSION_EXTRACT.endOffset(), 0, \"Number of this disk\");\n\n  /**\n   * Field in the record: the number of the disk where the central directory resides. This must be\n   * zero as multi-file archives are not supported.\n   */\n  private static final ZipField.F4 F_DISK_CD_START =\n      new ZipField.F4(F_NUMBER_OF_DISK.endOffset(), 0, \"Disk where CD starts\");\n\n  /**\n   * Field in the record: the number of entries in the Central Directory on this disk. Because we do\n   * not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}\n   */\n  private static final ZipField.F8 F_RECORDS_DISK =\n      new ZipField.F8(\n          F_DISK_CD_START.endOffset(),\n          \"Record on disk count\",\n          new ZipFieldInvariantNonNegative());\n\n  /** Field in the record: the total number of entries in the Central Directory. */\n  private static final ZipField.F8 F_RECORDS_TOTAL =\n      new ZipField.F8(\n          F_RECORDS_DISK.endOffset(),\n          \"Total records\",\n          new ZipFieldInvariantNonNegative());\n\n  /** Field in the record: number of bytes of the Central Directory. */\n  private static final ZipField.F8 F_CD_SIZE =\n      new ZipField.F8(\n          F_RECORDS_TOTAL.endOffset(), \"Directory size\", new ZipFieldInvariantNonNegative());\n\n  /** Field in the record: offset, from the archive start, where the Central Directory starts. */\n  private static final ZipField.F8 F_CD_OFFSET =\n      new ZipField.F8(\n          F_CD_SIZE.endOffset(), \"Directory offset\", new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in Version 2 of the record: The compression method used for the Central Directory in the\n   * given Zip file. Although we do support version 2 of the Zip64 EOCD, we presently do not support\n   * any compression method, and thus this value must be zero.\n   */\n  private static final ZipField.F2 F_V2_CD_COMPRESSION_METHOD =\n      new ZipField.F2(\n          F_CD_OFFSET.endOffset(), 0, \"Version 2: Directory Compression method\");\n\n  /**\n   * Field in Version 2 of the record: The compressed size of the Central Directory. As Compression\n   * is not supported for the CD, this value should always be the same as\n   * {@link #F_V2_CD_UNCOMPRESSED_SIZE}.\n   */\n  private static final ZipField.F8 F_V2_CD_COMPRESSED_SIZE =\n      new ZipField.F8(\n          F_V2_CD_COMPRESSION_METHOD.endOffset(),\n          \"Version 2: Directory Compressed Size\",\n          new ZipFieldInvariantNonNegative());\n\n  /** Field in Version 2 of the record: The uncompressed size of the Central Directory. */\n  private static final ZipField.F8 F_V2_CD_UNCOMPRESSED_SIZE =\n      new ZipField.F8(\n          F_V2_CD_COMPRESSED_SIZE.endOffset(),\n          \"Version 2: Directory Uncompressed Size\",\n          new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in Version 2 of the record: The ID for the type of encryption used to encrypt the Central\n   * directory. Since Central Directory encryption is not supported, this value has to be zero.\n   */\n  private static final ZipField.F2 F_V2_CD_ENCRYPTION_ID =\n      new ZipField.F2(\n          F_V2_CD_UNCOMPRESSED_SIZE.endOffset(),\n          0,\n          \"Version 2: Directory Encryption\");\n\n  /**\n   * Field in Version 2 of the record: The length of the encryption key for the encryption of the\n   * Central Directory given by {@link #F_V2_CD_ENCRYPTION_ID}. Since encryption of the Central\n   * Directory is not supported, this value has to be zero.\n   */\n  private static final ZipField.F2 F_V2_CD_ENCRYPTION_KEY_LENGTH =\n      new ZipField.F2(\n          F_V2_CD_ENCRYPTION_ID.endOffset(),\n          0,\n          \"Version 2: Directory Encryption key length\");\n\n  /**\n   * Field in Version 2 of the record: The flags for the encryption method used on the Central\n   * Directory. As encryption of the Central Directory is not supported, this value has to be zero.\n   */\n  private static final ZipField.F2 F_V2_CD_ENCRYPTION_FLAGS =\n      new ZipField.F2(\n          F_V2_CD_ENCRYPTION_KEY_LENGTH.endOffset(),\n          0,\n          \"Version 2: Directory Encryption Flags\");\n\n  /**\n   * Field in Version 2 of the record: ID of the algorithm used to hash the Central Directory data.\n   * Hashing of the Central Directory is not supported, so this value has to be zero.\n   */\n  private static final ZipField.F2 F_V2_HASH_ID =\n      new ZipField.F2(\n          F_V2_CD_ENCRYPTION_FLAGS.endOffset(),\n          0,\n          \"Version 2: Hash algorithm ID\");\n\n  /**\n   * Field in Version 2 of the record: Length of the data for the hash of the Central Directory.\n   * Hashing of the Central Directory is not supported, so this value has to be zero.\n   */\n  private static final ZipField.F2 F_V2_HASH_LENGTH =\n      new ZipField.F2(\n          F_V2_HASH_ID.endOffset(),\n          0,\n          \"Version 2: Hash length\");\n\n  /** The location of the Zip64 size field relative to the start of the Zip64 EOCD. */\n  public static final int SIZE_OFFSET = F_EOCD_SIZE.offset();\n\n  /**\n   * The difference between the size in the size field and the true size of the Zip64 EOCD. The size\n   * field in the EOCD does not consider the size field and the identifier field when calculating\n   * the size of the Zip64 EOCD record.\n   */\n  public static final int TRUE_SIZE_DIFFERENCE = F_EOCD_SIZE.endOffset();\n\n  /** Code of the program that made the zip. We actually don't care about this. */\n  private final long madeBy;\n\n  /** Version needed to extract the zip. */\n  private final long versionToExtract;\n\n  /** Number of entries in the Central Directory. */\n  private final long totalRecords;\n\n  /** Offset from the beginning of the archive where the Central Directory is located. */\n  private final long directoryOffset;\n\n  /** Number of bytes of the Central Directory. */\n  private final long directorySize;\n\n  /** The variable extra fields at the end of the Zip64 EOCD (in both Version 1 and 2). */\n  private final Zip64ExtensibleDataSector extraFields;\n\n  /** Supplier of the byte representation of the Zip64 EOCD. */\n  private final CachedSupplier<byte[]> byteSupplier;\n\n  /**\n   * Creates a Zip64Eocd record from the given information from the central directory record.\n   *\n   * @param totalRecords the number of entries in the central directory.\n   * @param directoryOffset the offset of the central directory from the start of the archive.\n   * @param directorySize the size (in bytes) of the central directory record.\n   * @param useVersion2 whether we want to use Version 2 of the Zip64 EOCD.\n   * @param dataSector the extensible data sector.\n   */\n  Zip64Eocd(\n      long totalRecords,\n      long directoryOffset,\n      long directorySize,\n      boolean useVersion2,\n      Zip64ExtensibleDataSector dataSector) {\n    this.madeBy = DEFAULT_VERSION_MADE_BY;\n    this.totalRecords = totalRecords;\n    this.directorySize = directorySize;\n    this.directoryOffset = directoryOffset;\n    this.versionToExtract =\n        useVersion2\n            ? CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION\n            : CentralDirectoryHeaderCompressInfo.VERSION_WITH_ZIP64_EXTENSIONS;\n    extraFields = dataSector;\n\n    byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n  }\n\n  /**\n   * Creates a Zip64 EOCD from the given byte information. It does verify that the record starts\n   * with the correct header information.\n   *\n   * @param bytes the bytes to be read as a Zip64 EOCD\n   * @throws IOException the bytes could not be read as a Zip64 EOCD\n   */\n  Zip64Eocd(ByteBuffer bytes) throws IOException {\n\n    F_SIGNATURE.verify(bytes);\n    long eocdSize = F_EOCD_SIZE.read(bytes);\n    long madeBy = F_MADE_BY.read(bytes);\n    long versionToExtract = F_VERSION_EXTRACT.read(bytes);\n    F_NUMBER_OF_DISK.verify(bytes);\n    F_DISK_CD_START.verify(bytes);\n    long totalRecords1 = F_RECORDS_DISK.read(bytes);\n    long totalRecords2 = F_RECORDS_TOTAL.read(bytes);\n    long directorySize = F_CD_SIZE.read(bytes);\n    long directoryOffset = F_CD_OFFSET.read(bytes);\n    long sizeOfFixedFields = F_CD_OFFSET.endOffset();\n\n    // sanity checks for Version 1 fields.\n    if (totalRecords1 != totalRecords2) {\n      throw new IOException(\n          \"Zip states records split in multiple disks, which is not supported\");\n    }\n\n    // read Version 2 fields if necessary\n    if (versionToExtract\n        >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION) {\n      if (eocdSize < F_V2_HASH_LENGTH.endOffset() - F_EOCD_SIZE.endOffset()) {\n        throw new IOException(\n            \"Zip states the size of Zip64 EOCD is too small for version 2 format.\");\n      }\n\n      F_V2_CD_COMPRESSION_METHOD.verify(bytes);\n      long compressedSize = F_V2_CD_COMPRESSED_SIZE.read(bytes);\n      long uncompressedSize = F_V2_CD_UNCOMPRESSED_SIZE.read(bytes);\n      F_V2_CD_ENCRYPTION_ID.verify(bytes);\n      F_V2_CD_ENCRYPTION_KEY_LENGTH.verify(bytes);\n      F_V2_CD_ENCRYPTION_FLAGS.verify(bytes);\n      F_V2_HASH_ID.verify(bytes);\n      F_V2_HASH_LENGTH.verify(bytes);\n      sizeOfFixedFields = F_V2_HASH_LENGTH.endOffset();\n\n      // sanity checks for version 2 fields.\n      if (compressedSize != uncompressedSize) {\n        throw new IOException(\n            \"Zip states Central Directory Compression is used, which is not supported\");\n      }\n      directorySize = uncompressedSize;\n    }\n\n    this.madeBy = madeBy;\n    this.versionToExtract = versionToExtract;\n    this.totalRecords = totalRecords1;\n    this.directorySize = directorySize;\n    this.directoryOffset = directoryOffset;\n\n    long extensibleDataSize = eocdSize - (sizeOfFixedFields - F_EOCD_SIZE.endOffset());\n\n    if (extensibleDataSize > Integer.MAX_VALUE) {\n      throw new IOException(\"Extensible data of size: \" + extensibleDataSize + \"not supported\");\n    }\n    byte[] rawData = new byte[Ints.checkedCast(extensibleDataSize)];\n    bytes.get(rawData);\n    extraFields = new Zip64ExtensibleDataSector(rawData);\n    byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n  }\n\n  /**\n   * The size of the fixed field in the Zip64 EOCD. This vaue may be different if we are using a\n   * version 1 or version 2 record.\n   *\n   * @return the size of the fixed fields.\n   */\n  private int sizeOfFixedFields() {\n    return versionToExtract\n        >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION\n          ? F_V2_HASH_LENGTH.endOffset()\n          : F_CD_OFFSET.endOffset();\n  }\n\n  /**\n   * Gets the size (in bytes) of the Zip64 EOCD record.\n   *\n   * @return the size of the record.\n   */\n  public int size() {\n    return sizeOfFixedFields() + extraFields.size();\n  }\n\n  public long getTotalRecords() {\n    return totalRecords;\n  }\n\n  public long getDirectorySize() {\n    return directorySize;\n  }\n\n  public long getDirectoryOffset() {\n    return directoryOffset;\n  }\n\n  public Zip64ExtensibleDataSector getExtraFields() {\n    return extraFields;\n  }\n\n  public long getVersionToExtract() { return versionToExtract; }\n\n  /**\n   * Gets the byte representation of The Zip64 EOCD record.\n   *\n   * @return the bytes of the EOCD.\n   */\n  public byte[] toBytes() {\n    return byteSupplier.get();\n  }\n\n  private byte[] computeByteRepresentation() {\n    int size = size();\n    ByteBuffer out = ByteBuffer.allocate(size);\n\n    try {\n      F_SIGNATURE.write(out);\n      F_EOCD_SIZE.write(out, size - F_EOCD_SIZE.endOffset());\n      F_MADE_BY.write(out, madeBy);\n      F_VERSION_EXTRACT.write(out, versionToExtract);\n      F_NUMBER_OF_DISK.write(out);\n      F_DISK_CD_START.write(out);\n      F_RECORDS_DISK.write(out, totalRecords);\n      F_RECORDS_TOTAL.write(out, totalRecords);\n      F_CD_SIZE.write(out, directorySize);\n      F_CD_OFFSET.write(out, directoryOffset);\n\n      // write version 2 fields if necessary.\n      if (versionToExtract\n          >= CentralDirectoryHeaderCompressInfo.VERSION_WITH_CENTRAL_FILE_ENCRYPTION) {\n        F_V2_CD_COMPRESSION_METHOD.write(out);\n        F_V2_CD_COMPRESSED_SIZE.write(out, directorySize);\n        F_V2_CD_UNCOMPRESSED_SIZE.write(out, directorySize);\n        F_V2_CD_ENCRYPTION_ID.write(out);\n        F_V2_CD_ENCRYPTION_KEY_LENGTH.write(out);\n        F_V2_CD_ENCRYPTION_FLAGS.write(out);\n        F_V2_HASH_ID.write(out);\n        F_V2_HASH_LENGTH.write(out);\n      }\n\n      extraFields.write(out);\n\n      return out.array();\n    } catch (IOException e) {\n      throw new IOExceptionWrapper(e);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Zip64EocdLocator.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.utils.CachedSupplier;\nimport com.android.tools.build.apkzlib.utils.IOExceptionWrapper;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.ByteBuffer;\n\n/**\n * Zip64 End of Central Directory Locator. Used to locate the Zip64 EOCD record in\n * the Zip64 format. This will be located right above the standard EOCD record, if it exists.\n */\nclass Zip64EocdLocator {\n  /** Field in the record: the record signature, fixed at this value by the specification. */\n  private static final ZipField.F4 F_SIGNATURE =\n      new ZipField.F4(0, 0x07064b50, \"Zip64 EOCD Locator signature\");\n\n  /**\n   * Field in the record: the number of the disk where the Zip64 EOCD is located. This has to be\n   * zero because multi-file archives are not supported.\n   */\n  private static final ZipField.F4 F_NUMBER_OF_DISK =\n      new ZipField.F4(F_SIGNATURE.endOffset(), 0, \"Number of disk with Zip64 EOCD\");\n\n  /**\n   * Field in the record: the location of the zip64 EOCD record on the disk specified by\n   * {@link #F_NUMBER_OF_DISK}.\n   */\n  private static final ZipField.F8 F_Z64_EOCD_OFFSET =\n      new ZipField.F8(\n          F_NUMBER_OF_DISK.endOffset(),\n          \"Offset of Zip64 EOCD\",\n          new ZipFieldInvariantNonNegative());\n\n  /**\n   * Field in the record: the total number of disks in the archive. This has to be zero because\n   * multi-file archives are not supported.\n   */\n  private static final ZipField.F4 F_TOTAL_NUMBER_OF_DISKS =\n      new ZipField.F4(\n          F_Z64_EOCD_OFFSET.endOffset(), 0,\"Total number of disks\");\n\n\n  public static final int LOCATOR_SIZE = F_TOTAL_NUMBER_OF_DISKS.endOffset();\n\n  /**\n   * Offset from the beginning of the archive to where the Zip64 End of Central Directory record\n   * is located.\n   */\n  private final long z64EocdOffset;\n\n  /** Supplier of the byte representation of the zip64 Eocd Locator. */\n  private final CachedSupplier<byte[]> byteSupplier;\n\n  /**\n   * Creates a new Zip64 EOCD Locator, reading it from a byte source. This method will parse the\n   * byte source and obtain the EOCD Locator. It will check that the byte source starts with the\n   * EOCD Locator signature.\n   *\n   * @param bytes the byte buffer with the Locator data; when this method finishes, the byte buffer\n   *     will have its position moved to the end of the Locator (the beginning of the standard EOCD)\n   * @throws IOException failed to read information or the EOCD data is corrupt or invalid.\n   */\n  Zip64EocdLocator(ByteBuffer bytes) throws IOException {\n    F_SIGNATURE.verify(bytes);\n    F_NUMBER_OF_DISK.verify(bytes);\n    long z64EocdOffset = F_Z64_EOCD_OFFSET.read(bytes);\n    F_TOTAL_NUMBER_OF_DISKS.verify(bytes);\n\n    Verify.verify(z64EocdOffset >= 0);\n    this.z64EocdOffset = z64EocdOffset;\n    byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n  }\n\n  /**\n   * Creates a new Zip64 EOCD Locator. This is used when generating an EOCD Locator for a\n   * Zip64 EOCD that has been generated.\n   *\n   * @param z64EocdOffset offset position of the Zip64 EOCD from the beginning of the archive\n   */\n  Zip64EocdLocator(long z64EocdOffset) {\n    Preconditions.checkArgument(z64EocdOffset >= 0, \"z64EocdOffset < 0\");\n\n    this.z64EocdOffset = z64EocdOffset;\n    byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);\n  }\n\n  /**\n   * Obtains the offset from the beginning of the archive to where the Zip64 EOCD is located.\n   *\n   * @return the Zip64 EOCD offset.\n   */\n  long getZ64EocdOffset() {\n    return z64EocdOffset;\n  }\n\n  /**\n   * Obtains the size of the Zip64 EOCD Locator\n   *\n   * @return the size, in bytes, of the EOCD Locator.<em> i.e. </em> 20.\n   */\n  long getSize() {\n    return F_TOTAL_NUMBER_OF_DISKS.endOffset();\n  }\n\n  /**\n   * Generates the EOCD Locator data.\n   *\n   * @return a byte representation of the EOCD Locator that has exactly {@link #getSize()} bytes\n   * @throws IOException failed to generate the EOCD data.\n   */\n  byte[] toBytes() throws IOException {\n    return byteSupplier.get();\n  }\n\n  /**\n   * Computes the byte representation of the EOCD Locator.\n   *\n   * @return a byte representation of the Zip64 EOCD Locator that has exactly {@link #getSize()}\n   *     bytes\n   * @throws UncheckedIOException failed to generate the EOCD Locator data\n   */\n  private byte[] computeByteRepresentation() {\n    ByteBuffer out = ByteBuffer.allocate(F_TOTAL_NUMBER_OF_DISKS.endOffset());\n\n    try {\n      F_SIGNATURE.write(out);\n      F_NUMBER_OF_DISK.write(out);\n      F_Z64_EOCD_OFFSET.write(out, z64EocdOffset);\n      F_TOTAL_NUMBER_OF_DISKS.write(out);\n\n      return out.array();\n    } catch (IOException e) {\n      throw new IOExceptionWrapper(e);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/Zip64ExtensibleDataSector.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.primitives.Ints;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.List;\nimport javax.annotation.Nullable;\n\n/**\n * Contains the special purpose data for the Zip64 EOCD record.\n *\n * <p>According to the zip specification, the Zip64 EOCD is composed of a sequence of zero or more\n * Special Purpose Data fields. This class provides a way to access, parse, and modify that\n * information.\n *\n * <p>Each Special Purpose Data is represented by an instance of {@link Z64SpecialPurposeData} and\n * contains a header ID and data.\n */\npublic class Zip64ExtensibleDataSector {\n\n  /**\n   * The extensible data sector's raw data, if it is known. Either this variable or {@link #fields}\n   * must be non-{@code null}.\n   */\n  @Nullable private final byte[] rawData;\n\n  /**\n   * The list of fields in the data sector. Will be populated if the data sector is created based on\n   * a list of special purpose data; will also be populated after parsing if the Data Sector is\n   * created based on the raw bytes.\n   */\n  @Nullable private ImmutableList<Z64SpecialPurposeData> fields;\n\n  /**\n   * Creates a Zip64 Extensible Data Sector based on existing raw data.\n   *\n   * @param rawData the raw data; will only be parsed if needed.\n   */\n  public Zip64ExtensibleDataSector(byte[] rawData) {\n    this.rawData = rawData;\n    fields = null;\n  }\n\n  /**\n   * Creates an Extensible Data Sector with no special purpose data.\n   */\n  public Zip64ExtensibleDataSector() {\n    rawData = null;\n    fields = ImmutableList.of();\n  }\n\n  /**\n   * Creates a Zip64 Extensible Data with the given Special purpose data.\n   *\n   * @param fields all special purpose data.\n   */\n  public Zip64ExtensibleDataSector(ImmutableList<Z64SpecialPurposeData> fields) {\n    rawData = null;\n    this.fields = fields;\n  }\n\n  int size() {\n    if (rawData != null) {\n      return rawData.length;\n    } else {\n      Preconditions.checkNotNull(fields);\n      int sumSizes = 0;\n      for (Z64SpecialPurposeData data : fields){\n        sumSizes += data.size();\n      }\n      return sumSizes;\n    }\n  }\n\n  void write(ByteBuffer out) throws IOException {\n    if (rawData != null) {\n      out.put(rawData);\n    } else {\n      Preconditions.checkNotNull(fields);\n      for (Z64SpecialPurposeData data : fields) {\n        data.write(out);\n      }\n    }\n  }\n\n  public ImmutableList<Z64SpecialPurposeData> getFields() throws IOException {\n    if (fields == null) {\n      parseData();\n    }\n\n    Preconditions.checkNotNull(fields);\n    return fields;\n  }\n\n  private void parseData() throws IOException {\n    Preconditions.checkNotNull(rawData);\n    Preconditions.checkState(fields == null);\n\n    List<Z64SpecialPurposeData> fields = new ArrayList<>();\n    ByteBuffer buffer = ByteBuffer.wrap(rawData);\n\n    while (buffer.remaining() > 0) {\n      int headerId = LittleEndianUtils.readUnsigned2Le(buffer);\n      long dataSize = LittleEndianUtils.readUnsigned4Le(buffer);\n\n      byte[] data = new byte[Ints.checkedCast(dataSize)];\n      if (dataSize < 0) {\n        throw new IOException(\n            \"Invalid data size for special purpose data with header ID \"\n                + headerId\n                + \": \"\n                + dataSize);\n      }\n      buffer.get(data);\n\n      SpecialPurposeDataFactory factory = RawSpecialPurposeData::new;\n      Z64SpecialPurposeData spd = factory.make(headerId, data);\n      fields.add(spd);\n    }\n    this.fields = ImmutableList.copyOf(fields);\n  }\n\n  public interface Z64SpecialPurposeData {\n\n    /** Length of header id and the size length fields that comes before the data */\n    int PREFIX_LENGTH = 6;\n\n    /**\n     * Obtains the Special purpose data's header id.\n     *\n     * @return the data's header id.\n     */\n    int getHeaderId();\n\n    /**\n     * Obtains the size of the data in this special purpose data.\n     *\n     * @return the number of bytes needed to write the data.\n     */\n    int size();\n\n    /**\n     * Writes the special purpose data to the buffer.\n     *\n     * @param out the buffer where to write the data to; exactly {@link #size()} bytes will be\n     *     written.\n     * @throws IOException failed to write special purpose data.\n     */\n    void write(ByteBuffer out) throws IOException;\n  }\n\n  public  interface SpecialPurposeDataFactory {\n\n    /**\n     * Creates a new special purpose data.\n     *\n     * @param headerId the header ID\n     * @param data the data in the special purpose data\n     * @return the created special purpose data.\n     * @throws IOException failed to create the special purpose data from the data given.\n     */\n    Z64SpecialPurposeData make(int headerId, byte[] data) throws IOException;\n  }\n\n  /**\n   * Special Purpose Data containing raw data: this class represents a general \"special purpose\n   * data\" containing an array of bytes as data.\n   */\n  public static class RawSpecialPurposeData implements Z64SpecialPurposeData {\n\n    /** Header ID. */\n    private final int headerId;\n\n    /** Data in the segment */\n    private final byte[] data;\n\n    RawSpecialPurposeData(int headerId, byte[] data) {\n      this.headerId = headerId;\n      this.data = data;\n    }\n\n    @Override\n    public int getHeaderId() {\n      return headerId;\n    }\n\n    @Override\n    public int size() {\n      return PREFIX_LENGTH + data.length;\n    }\n\n    @Override\n    public void write(ByteBuffer out) throws IOException {\n      LittleEndianUtils.writeUnsigned2Le(out, headerId);\n      LittleEndianUtils.writeUnsigned4Le(out, data.length);\n      out.put(data);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipField.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\nimport com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils;\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport com.google.common.collect.Sets;\nimport com.google.common.primitives.Ints;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Set;\nimport javax.annotation.Nullable;\n\n/**\n * The ZipField class represents a field in a record in a zip file. Zip files are made with records\n * that have fields. This class makes it easy to read, write and verify field values.\n *\n * <p>There are three main types of fields: 2-byte, 4-byte, and 8-byte fields. We represent each\n * one as a subclass of {@code ZipField}, {@code F2} for the 2-byte field, {@code F4} for the 4-byte\n * field and {@code F8} for the 8-byte field. Because Java's {@code int} data type is guaranteed to\n * be 4-byte, all methods use Java's native {@link int} as data type.\n *\n * <p>The {@code F8} subclass is to support the 8-byte fields in the Zip64 specification. Because\n * Java's 8-byte {@code long} does not support unsigned types, which reduces the support to 8-byte\n * numbers of the form 2^63-1 or less. As {@code F8} fields refer to file sizes, this should be\n * sufficient.\n *\n * <p>For each field we can either read, write or verify. Verification is used for fields whose\n * value we know. Some fields, <em>e.g.</em> signature fields, have fixed value. Other fields have\n * variable values, but in some situations we know which value they have. For example, the last\n * modification time of a file's local header will have to match the value of the file's\n * modification time as stored in the central directory.\n *\n * <p>Because records are compact, <em>i.e.</em> fields are stored sequentially with no empty\n * spaces, fields are generally created in the sequence they exist and the end offset of a field is\n * used as the offset of the next one. The end of a field can be obtained by invoking {@link\n * #endOffset()}. This allows creating fields in sequence without doing offset computation:\n *\n * <pre>\n * ZipField.F2 firstField = new ZipField.F2(0, \"First field\");\n * ZipField.F4 secondField = new ZipField(firstField.endOffset(), \"Second field\");\n * ZipField.F8 thirdField = new ZipField(secondField.endOffset(), \"Third field\");\n * </pre>\n */\nabstract class ZipField {\n\n  /** Field name. Used for providing (more) useful error messages. */\n  private final String name;\n\n  /** Offset of the file in the record. */\n  protected final int offset;\n\n  /** Size of the field. Only 2, 4, or 8 allowed. */\n  private final int size;\n\n  /** If a fixed value exists for the field, then this attribute will contain that value. */\n  @Nullable private final Long expected;\n\n  /** All invariants that this field must verify. */\n  private final Set<ZipFieldInvariant> invariants;\n\n  /**\n   * Creates a new field that does not contain a fixed value.\n   *\n   * @param offset the field's offset in the record\n   * @param size the field size\n   * @param name the field's name\n   * @param invariants the invariants that must be verified by the field\n   */\n  ZipField(int offset, int size, String name, ZipFieldInvariant... invariants) {\n    Preconditions.checkArgument(offset >= 0, \"offset >= 0\");\n    Preconditions.checkArgument(\n        size == 2 || size == 4 || size == 8,\n        \"size != 2 && size != 4 && size != 8\");\n\n    this.name = name;\n    this.offset = offset;\n    this.size = size;\n    expected = null;\n    this.invariants = Sets.newHashSet(invariants);\n  }\n\n  /**\n   * Creates a new field that contains a fixed value.\n   *\n   * @param offset the field's offset in the record\n   * @param size the field size\n   * @param expected the expected field value\n   * @param name the field's name\n   */\n  ZipField(int offset, int size, long expected, String name) {\n    Preconditions.checkArgument(offset >= 0, \"offset >= 0\");\n    Preconditions.checkArgument(\n        size == 2 || size == 4 || size == 8,\n        \"size != 2 && size != 4 && size != 8\");\n\n    this.name = name;\n    this.offset = offset;\n    this.size = size;\n    this.expected = expected;\n    invariants = Sets.newHashSet();\n  }\n\n  /**\n   * Checks whether a value verifies the field's invariants. Nothing happens if the value verifies\n   * the invariants.\n   *\n   * @param value the value\n   * @throws IOException the invariants are not verified\n   */\n  private void checkVerifiesInvariants(long value) throws IOException {\n    for (ZipFieldInvariant invariant : invariants) {\n      if (!invariant.isValid(value)) {\n        throw new IOException(\n            \"Value \"\n                + value\n                + \" of field \"\n                + name\n                + \" is invalid \"\n                + \"(fails '\"\n                + invariant.getName()\n                + \"').\");\n      }\n    }\n  }\n\n  /**\n   * Advances the position in the provided byte buffer by the size of this field.\n   *\n   * @param bytes the byte buffer; at the end of the method its position will be greater by the size\n   *     of this field\n   * @throws IOException failed to advance the buffer\n   */\n  void skip(ByteBuffer bytes) throws IOException {\n    if (bytes.remaining() < size) {\n      throw new IOException(\n          \"Cannot skip field \"\n              + name\n              + \" because only \"\n              + bytes.remaining()\n              + \" remain in the buffer.\");\n    }\n\n    bytes.position(bytes.position() + size);\n  }\n\n  /**\n   * Reads a field value.\n   *\n   * @param bytes the byte buffer with the record data; after this method finishes, the buffer will\n   *     be positioned at the first byte after the field\n   * @return the value of the field\n   * @throws IOException failed to read the field\n   */\n  long read(ByteBuffer bytes) throws IOException {\n    if (bytes.remaining() < size) {\n      throw new IOException(\n          \"Cannot skip field \"\n              + name\n              + \" because only \"\n              + bytes.remaining()\n              + \" remain in the buffer.\");\n    }\n\n    bytes.order(ByteOrder.LITTLE_ENDIAN);\n\n    long r;\n    if (size == 2) {\n      r = LittleEndianUtils.readUnsigned2Le(bytes);\n    } else if (size == 4) {\n      r = LittleEndianUtils.readUnsigned4Le(bytes);\n    } else {\n      r = LittleEndianUtils.readUnsigned8Le(bytes);\n    }\n\n    checkVerifiesInvariants(r);\n    return r;\n  }\n\n  /**\n   * Verifies that the field at the current buffer position has the expected value. The field must\n   * have been created with the constructor that defines the expected value.\n   *\n   * @param bytes the byte buffer with the record data; after this method finishes, the buffer will\n   *     be positioned at the first byte after the field\n   * @throws IOException failed to read the field or the field does not have the expected value\n   */\n  void verify(ByteBuffer bytes) throws IOException {\n    verify(bytes, null);\n  }\n\n  /**\n   * Verifies that the field at the current buffer position has the expected value. The field must\n   * have been created with the constructor that defines the expected value.\n   *\n   * @param bytes the byte buffer with the record data; after this method finishes, the buffer will\n   *     be positioned at the first byte after the field\n   * @param verifyLog if non-{@code null}, will log the verification error\n   * @throws IOException failed to read the data or the field does not have the expected value; only\n   *     thrown if {@code verifyLog} is {@code null}\n   */\n  void verify(ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException {\n    Preconditions.checkState(expected != null, \"expected == null\");\n    verify(bytes, expected, verifyLog);\n  }\n\n  /**\n   * Verifies that the field has an expected value.\n   *\n   * @param bytes the byte buffer with the record data; after this method finishes, the buffer will\n   *     be positioned at the first byte after the field\n   * @param expected the value we expect the field to have; if this field has invariants, the value\n   *     must verify them\n   * @throws IOException failed to read the data or the field does not have the expected value\n   */\n  void verify(ByteBuffer bytes, long expected) throws IOException {\n    verify(bytes, expected, null);\n  }\n\n  /**\n   * Verifies that the field has an expected value.\n   *\n   * @param bytes the byte buffer with the record data; after this method finishes, the buffer will\n   *     be positioned at the first byte after the field\n   * @param expected the value we expect the field to have; if this field has invariants, the value\n   *     must verify them\n   * @param verifyLog if non-{@code null}, will log the verification error\n   * @throws IOException failed to read the data or the field does not have the expected value; only\n   *     thrown if {@code verifyLog} is {@code null}\n   */\n  void verify(ByteBuffer bytes, long expected, @Nullable VerifyLog verifyLog) throws IOException {\n    checkVerifiesInvariants(expected);\n    long r = read(bytes);\n    if (r != expected) {\n      String error =\n          String.format(\n              \"Incorrect value for field '%s': value is %s but %s expected.\", name, r, expected);\n\n      if (verifyLog == null) {\n        throw new IOException(error);\n      } else {\n        verifyLog.log(error);\n      }\n    }\n  }\n\n  /**\n   * Writes the value of the field.\n   *\n   * @param output where to write the field; the field will be written at the current position of\n   *     the buffer\n   * @param value the value to write\n   * @throws IOException failed to write the value in the stream\n   */\n  void write(ByteBuffer output, long value) throws IOException {\n    checkVerifiesInvariants(value);\n\n    Preconditions.checkArgument(value >= 0, \"value (%s) < 0\", value);\n\n    if (size == 2) {\n      Preconditions.checkArgument(value <= 0x0000ffff, \"value (%s) > 0x0000ffff\", value);\n      LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value));\n    } else if (size == 4) {\n      Preconditions.checkArgument(\n          value <= 0x00000000ffffffffL, \"value (%s) > 0x00000000ffffffffL\", value);\n      LittleEndianUtils.writeUnsigned4Le(output, value);\n    } else {\n      Verify.verify(size == 8);\n      LittleEndianUtils.writeUnsigned8Le(output, value);\n    }\n  }\n\n  /**\n   * Writes the value of the field. The field must have an expected value set in the constructor.\n   *\n   * @param output where to write the field; the field will be written at the current position of\n   *     the buffer\n   * @throws IOException failed to write the value in the stream\n   */\n  void write(ByteBuffer output) throws IOException {\n    Preconditions.checkState(expected != null, \"expected == null\");\n    write(output, expected);\n  }\n\n  /**\n   * Obtains the offset at which the field starts.\n   *\n   * @return the start offset\n   */\n  int offset() {\n    return offset;\n  }\n\n  /**\n   * Obtains the offset at which the field ends. This is the exact offset at which the next field\n   * starts.\n   *\n   * @return the end offset\n   */\n  int endOffset() {\n    return offset + size;\n  }\n\n  /** Concrete implementation of {@link ZipField} that represents a 2-byte field. */\n  static class F2 extends ZipField {\n\n    /**\n     * Creates a new field.\n     *\n     * @param offset the field's offset in the record\n     * @param name the field's name\n     * @param invariants the invariants that must be verified by the field\n     */\n    F2(int offset, String name, ZipFieldInvariant... invariants) {\n      super(offset, 2, name, invariants);\n    }\n\n    /**\n     * Creates a new field that contains a fixed value.\n     *\n     * @param offset the field's offset in the record\n     * @param expected the expected field value\n     * @param name the field's name\n     */\n    F2(int offset, long expected, String name) {\n      super(offset, 2, expected, name);\n    }\n  }\n\n  /** Concrete implementation of {@link ZipField} that represents a 4-byte field. */\n  static class F4 extends ZipField {\n    /**\n     * Creates a new field.\n     *\n     * @param offset the field's offset in the record\n     * @param name the field's name\n     * @param invariants the invariants that must be verified by the field\n     */\n    F4(int offset, String name, ZipFieldInvariant... invariants) {\n      super(offset, 4, name, invariants);\n    }\n\n    /**\n     * Creates a new field that contains a fixed value.\n     *\n     * @param offset the field's offset in the record\n     * @param expected the expected field value\n     * @param name the field's name\n     */\n    F4(int offset, long expected, String name) {\n      super(offset, 4, expected, name);\n    }\n  }\n\n  /** Concrete implementation of {@link ZipField} that represents a 8-byte field. */\n  static class F8 extends ZipField {\n\n    /**\n     * Creates a new field\n     *\n     * @param offset offset the field's offset in the record\n     * @param name the field's name\n     * @param invariants the invariants that must be verified by the field\n     */\n    F8(int offset, String name, ZipFieldInvariant... invariants) {\n      super(offset, 8, name, invariants);\n    }\n\n    /**\n     * Creates a new field that contains a fixed value.\n     *\n     * @param offset the field's offset in the record\n     * @param expected the expected field value\n     * @param name the field's name\n     */\n    F8(int offset, long expected, String name) {\n      super(offset, 8, expected, name);\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariant.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/**\n * A field rule defines an invariant (<em>i.e.</em>, a constraint) that has to be verified by a\n * field value.\n */\ninterface ZipFieldInvariant {\n\n  /**\n   * Evalutes the invariant against a value.\n   *\n   * @param value the value to check the invariant\n   * @return is the invariant valid?\n   */\n  boolean isValid(long value);\n\n  /**\n   * Obtains the name of the invariant. Used for information purposes.\n   *\n   * @return the name of the invariant\n   */\n  String getName();\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMaxValue.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/** Invariant checking a zip field does not exceed a threshold. */\nclass ZipFieldInvariantMaxValue implements ZipFieldInvariant {\n\n  /** The maximum value allowed. */\n  private final long max;\n\n  /**\n   * Creates a new invariant.\n   *\n   * @param max the maximum value allowed for the field\n   */\n  ZipFieldInvariantMaxValue(long max) {\n    this.max = max;\n  }\n\n  @Override\n  public boolean isValid(long value) {\n    return value <= max;\n  }\n\n  @Override\n  public String getName() {\n    return \"Maximum value \" + max;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantMinValue.java",
    "content": "/*\n * Copyright (C) 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/** Invariant checking a zip field doesn't go below a given value.*/\nclass ZipFieldInvariantMinValue implements ZipFieldInvariant {\n\n  /** The minimum value allowed. */\n  private final long min;\n\n  /**\n   * Creates a new invariant.\n   *\n   * @param min the minimum value allowed for the field\n   */\n  ZipFieldInvariantMinValue(long min) {\n    this.min = min;\n  }\n\n  @Override\n  public boolean isValid(long value) {\n    return value >= min;\n  }\n\n  @Override\n  public String getName() {\n    return \"Min value \" + min;\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFieldInvariantNonNegative.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/** Invariant that verifies a field's value is not negative. */\nclass ZipFieldInvariantNonNegative implements ZipFieldInvariant {\n\n  @Override\n  public boolean isValid(long value) {\n    return value >= 0;\n  }\n\n  @Override\n  public String getName() {\n    return \"Is positive\";\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/ZipFileState.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip;\n\n/** The {@code ZipFileState} enumeration holds the state of a {@link ZFile}. */\nenum ZipFileState {\n  /** Zip file is closed. */\n  CLOSED,\n\n  /** File file is open in read-only mode. */\n  OPEN_RO,\n\n  /** File file is open in read-write mode. */\n  OPEN_RW\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.compress;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.zip.CompressionResult;\nimport com.android.tools.build.apkzlib.zip.utils.ByteTracker;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.base.Preconditions;\nimport java.util.concurrent.Executor;\nimport java.util.zip.Deflater;\n\n/**\n * Compressor that tries both the best and default compression algorithms and picks the default\n * unless the best is at least a given percentage smaller.\n */\npublic class BestAndDefaultDeflateExecutorCompressor extends ExecutorCompressor {\n\n  /** Deflater using the default compression level. */\n  private final DeflateExecutionCompressor defaultDeflater;\n\n  /** Deflater using the best compression level. */\n  private final DeflateExecutionCompressor bestDeflater;\n\n  /**\n   * Minimum best compression size / default compression size ratio needed to pick the default\n   * compression size.\n   */\n  private final double minRatio;\n\n  /**\n   * Creates a new compressor.\n   *\n   * @param executor the executor used to perform compression activities.\n   * @param minRatio the minimum best compression size / default compression size needed to pick the\n   *     default compression size; if {@code 0.0} then the default compression is always picked, if\n   *     {@code 1.0} then the best compression is always picked unless it produces the exact same\n   *     size as the default compression.\n   */\n  public BestAndDefaultDeflateExecutorCompressor(Executor executor, double minRatio) {\n    super(executor);\n\n    Preconditions.checkArgument(minRatio >= 0.0, \"minRatio < 0.0\");\n    Preconditions.checkArgument(minRatio <= 1.0, \"minRatio > 1.0\");\n\n    defaultDeflater = new DeflateExecutionCompressor(executor, Deflater.DEFAULT_COMPRESSION);\n    bestDeflater = new DeflateExecutionCompressor(executor, Deflater.BEST_COMPRESSION);\n    this.minRatio = minRatio;\n  }\n\n  @Deprecated\n  public BestAndDefaultDeflateExecutorCompressor(\n      Executor executor, ByteTracker tracker, double minRatio) {\n    this(executor, minRatio);\n  }\n\n  @Override\n  protected CompressionResult immediateCompress(CloseableByteSource source, ByteStorage storage)\n      throws Exception {\n    CompressionResult defaultResult = defaultDeflater.immediateCompress(source, storage);\n    CompressionResult bestResult = bestDeflater.immediateCompress(source, storage);\n\n    double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize();\n    if (sizeRatio >= minRatio) {\n      return defaultResult;\n    } else {\n      return bestResult;\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/DeflateExecutionCompressor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.compress;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.bytestorage.CloseableByteSourceFromOutputStreamBuilder;\nimport com.android.tools.build.apkzlib.zip.CompressionMethod;\nimport com.android.tools.build.apkzlib.zip.CompressionResult;\nimport com.android.tools.build.apkzlib.zip.utils.ByteTracker;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.io.ByteStreams;\nimport java.io.InputStream;\nimport java.util.concurrent.Executor;\nimport java.util.zip.Deflater;\nimport java.util.zip.DeflaterOutputStream;\n\n/** Compressor that uses deflate with an executor. */\npublic class DeflateExecutionCompressor extends ExecutorCompressor {\n\n  /** Deflate compression level. */\n  private final int level;\n\n  /**\n   * Creates a new compressor.\n   *\n   * @param executor the executor to run deflation tasks\n   * @param level the compression level\n   */\n  public DeflateExecutionCompressor(Executor executor, int level) {\n    super(executor);\n\n    this.level = level;\n  }\n\n  @Deprecated\n  public DeflateExecutionCompressor(Executor executor, ByteTracker tracker, int level) {\n    this(executor, level);\n  }\n\n  @Override\n  protected CompressionResult immediateCompress(CloseableByteSource source, ByteStorage storage)\n      throws Exception {\n    Deflater deflater = new Deflater(level, true);\n    CloseableByteSourceFromOutputStreamBuilder resultBuilder = storage.makeBuilder();\n\n    try (InputStream inputStream = source.openBufferedStream();\n            DeflaterOutputStream dos = new DeflaterOutputStream(resultBuilder, deflater)) {\n      ByteStreams.copy(inputStream, dos);\n    }\n\n    CloseableByteSource result = resultBuilder.build();\n    if (result.size() >= source.size()) {\n      result.close();\n      return new CompressionResult(source, CompressionMethod.STORE, source.size());\n    } else {\n      return new CompressionResult(result, CompressionMethod.DEFLATE, result.size());\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/ExecutorCompressor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.compress;\n\nimport com.android.tools.build.apkzlib.bytestorage.ByteStorage;\nimport com.android.tools.build.apkzlib.zip.CompressionResult;\nimport com.android.tools.build.apkzlib.zip.Compressor;\nimport com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;\nimport com.google.common.util.concurrent.ListenableFuture;\nimport com.google.common.util.concurrent.SettableFuture;\nimport java.util.concurrent.Executor;\n\n/**\n * A synchronous compressor is a compressor that computes the result of compression immediately and\n * never returns an uncomputed future object.\n */\npublic abstract class ExecutorCompressor implements Compressor {\n\n  /** The executor that does the work. */\n  private final Executor executor;\n\n  /**\n   * Compressor that delegates execution into the given executor.\n   *\n   * @param executor the executor that will do the compress\n   */\n  public ExecutorCompressor(Executor executor) {\n    this.executor = executor;\n  }\n\n  @Override\n  public ListenableFuture<CompressionResult> compress(\n      CloseableByteSource source, ByteStorage storage) {\n    final SettableFuture<CompressionResult> future = SettableFuture.create();\n    executor.execute(\n        () -> {\n          try {\n            future.set(immediateCompress(source, storage));\n          } catch (Throwable e) {\n            future.setException(e);\n          }\n        });\n\n    return future;\n  }\n\n  /**\n   * Immediately compresses a source.\n   *\n   * @param source the source to compress\n   * @param storage a byte storage where the compressor can obtain data sources from\n   * @return the result of compression\n   * @throws Exception failed to compress\n   */\n  protected abstract CompressionResult immediateCompress(\n      CloseableByteSource source, ByteStorage storage) throws Exception;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/Zip64NotSupportedException.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.compress;\n\nimport java.io.IOException;\n\n/** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */\npublic class Zip64NotSupportedException extends IOException {\n\n  public Zip64NotSupportedException(String message) {\n    super(message);\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/compress/package-info.java",
    "content": "/*\n * Copyright (C) 2017 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\n/** Compressors to use with the {@code zip} package. */\npackage com.android.tools.build.apkzlib.zip.compress;\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/ByteTracker.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.utils;\n\n\n/**\n * Keeps track of used bytes allowing gauging memory usage.\n *\n * @deprecated will be removed shortly.\n */\n@Deprecated\npublic class ByteTracker {\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableByteSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.utils;\n\nimport com.google.common.io.ByteSource;\nimport java.io.Closeable;\nimport java.io.IOException;\n\n/**\n * Byte source that can be closed. Closing a byte source allows releasing any resources associated\n * with it. This should not be confused with closing streams. For example, {@link ByteTracker} uses\n * {@code CloseableByteSources} to know when the data associated with the byte source can be\n * released.\n */\npublic abstract class CloseableByteSource extends ByteSource implements Closeable {\n\n  /** Has the source been closed? */\n  private boolean closed;\n\n  /** Creates a new byte source. */\n  public CloseableByteSource() {\n    closed = false;\n  }\n\n  @Override\n  public final synchronized void close() throws IOException {\n    if (closed) {\n      return;\n    }\n\n    try {\n      innerClose();\n    } finally {\n      closed = true;\n    }\n  }\n\n  /**\n   * Closes the by source. This method is only invoked once, even if {@link #close()} is called\n   * multiple times.\n   *\n   * @throws IOException failed to close\n   */\n  protected abstract void innerClose() throws IOException;\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/CloseableDelegateByteSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.utils;\n\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.HashFunction;\nimport com.google.common.io.ByteProcessor;\nimport com.google.common.io.ByteSink;\nimport com.google.common.io.ByteSource;\nimport com.google.common.io.CharSource;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.Charset;\nimport javax.annotation.Nullable;\n\n/** Closeable byte source that delegates to another byte source. */\npublic class CloseableDelegateByteSource extends CloseableByteSource {\n\n  /** The byte source we delegate all operations to. {@code null} if disposed. */\n  @Nullable private ByteSource inner;\n\n  /**\n   * Size of the byte source. This is the same as {@code inner.size()} (when {@code inner} is not\n   * {@code null}), but we keep it separate to avoid calling {@code inner.size()} because it might\n   * throw {@code IOException}.\n   */\n  private final long mSize;\n\n  /**\n   * Creates a new byte source.\n   *\n   * @param inner the inner byte source\n   * @param size the size of the source\n   */\n  public CloseableDelegateByteSource(ByteSource inner, long size) {\n    this.inner = inner;\n    mSize = size;\n  }\n\n  /**\n   * Obtains the inner byte source. Will throw an exception if the inner by byte source has been\n   * disposed of.\n   *\n   * @return the inner byte source\n   */\n  private synchronized ByteSource get() {\n    if (inner == null) {\n      throw new ByteSourceDisposedException();\n    }\n\n    return inner;\n  }\n\n  /** Mark the byte source as disposed. */\n  @Override\n  protected synchronized void innerClose() throws IOException {\n    if (inner == null) {\n      return;\n    }\n\n    inner = null;\n  }\n\n  /**\n   * Obtains the size of this byte source. Equivalent to {@link #size()} but not throwing {@code\n   * IOException}.\n   *\n   * @return the size of the byte source\n   */\n  public long sizeNoException() {\n    return mSize;\n  }\n\n  @Override\n  public CharSource asCharSource(Charset charset) {\n    return get().asCharSource(charset);\n  }\n\n  @Override\n  public InputStream openBufferedStream() throws IOException {\n    return get().openBufferedStream();\n  }\n\n  @Override\n  public ByteSource slice(long offset, long length) {\n    return get().slice(offset, length);\n  }\n\n  @Override\n  public boolean isEmpty() throws IOException {\n    return get().isEmpty();\n  }\n\n  @Override\n  public long size() throws IOException {\n    return get().size();\n  }\n\n  @Override\n  public long copyTo(OutputStream output) throws IOException {\n    return get().copyTo(output);\n  }\n\n  @Override\n  public long copyTo(ByteSink sink) throws IOException {\n    return get().copyTo(sink);\n  }\n\n  @Override\n  public byte[] read() throws IOException {\n    return get().read();\n  }\n\n  @Override\n  public <T> T read(ByteProcessor<T> processor) throws IOException {\n    return get().read(processor);\n  }\n\n  @Override\n  public HashCode hash(HashFunction hashFunction) throws IOException {\n    return get().hash(hashFunction);\n  }\n\n  @Override\n  public boolean contentEquals(ByteSource other) throws IOException {\n    return get().contentEquals(other);\n  }\n\n  @Override\n  public InputStream openStream() throws IOException {\n    return get().openStream();\n  }\n\n  /** Exception thrown when trying to use a byte source that has been disposed. */\n  private static class ByteSourceDisposedException extends RuntimeException {\n\n    /** Creates a new exception. */\n    private ByteSourceDisposedException() {\n      super(\n          \"Byte source was created by a ByteTracker and is now disposed. If you see \"\n              + \"this message, then there is a bug.\");\n    }\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/LittleEndianUtils.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.utils;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.base.Verify;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\n/**\n * Utilities to read and write 16, 32, and 64 bit integers with support for little-endian encoding,\n * as used in zip files. Zip files actually use unsigned data types. We use Java's native (signed)\n * data types but will use long (64 bit) to ensure we can fit the whole range for the 16 and 32\n * bit fields.\n */\npublic class LittleEndianUtils {\n  /** Utility class, no constructor. */\n  private LittleEndianUtils() {}\n\n  /**\n   * Reads 8 bytes in little-endian format and converts them into a 64-bit value.\n   *\n   * @param bytes from where should the bytes be read; the first 8 bytes of the source will be read.\n   * @return the 64-bit value\n   * @throws IOException failed to read the value.\n   */\n  public static long readUnsigned8Le(ByteBuffer bytes) throws IOException {\n    Preconditions.checkNotNull(bytes, \"bytes == null\");\n\n    if (bytes.remaining() < 8) {\n      throw new EOFException(\n              \"Not enough data: 8 bytes expected, \" + bytes.remaining() + \" available.\");\n    }\n\n    ByteOrder order = bytes.order();\n    bytes.order(ByteOrder.LITTLE_ENDIAN);\n    long r = bytes.getLong();\n    bytes.order(order);\n    return r;\n  }\n\n  /**\n   * Reads 4 bytes in little-endian format and converts them into a 32-bit value.\n   *\n   * @param bytes from where should the bytes be read; the first 4 bytes of the source will be read\n   * @return the 32-bit value\n   * @throws IOException failed to read the value\n   */\n  public static long readUnsigned4Le(ByteBuffer bytes) throws IOException {\n    Preconditions.checkNotNull(bytes, \"bytes == null\");\n\n    if (bytes.remaining() < 4) {\n      throw new EOFException(\n          \"Not enough data: 4 bytes expected, \" + bytes.remaining() + \" available.\");\n    }\n\n    byte b0 = bytes.get();\n    byte b1 = bytes.get();\n    byte b2 = bytes.get();\n    byte b3 = bytes.get();\n    long r = (b0 & 0xff) | ((b1 & 0xff) << 8) | ((b2 & 0xff) << 16) | ((b3 & 0xffL) << 24);\n    Verify.verify(r >= 0);\n    Verify.verify(r <= 0x00000000ffffffffL);\n    return r;\n  }\n\n  /**\n   * Reads 2 bytes in little-endian format and converts them into a 16-bit value.\n   *\n   * @param bytes from where should the bytes be read; the first 2 bytes of the source will be read\n   * @return the 16-bit value\n   * @throws IOException failed to read the value\n   */\n  public static int readUnsigned2Le(ByteBuffer bytes) throws IOException {\n    Preconditions.checkNotNull(bytes, \"bytes == null\");\n\n    if (bytes.remaining() < 2) {\n      throw new EOFException(\n          \"Not enough data: 2 bytes expected, \" + bytes.remaining() + \" available.\");\n    }\n\n    byte b0 = bytes.get();\n    byte b1 = bytes.get();\n    int r = (b0 & 0xff) | ((b1 & 0xff) << 8);\n\n    Verify.verify(r >= 0);\n    Verify.verify(r <= 0x0000ffff);\n    return r;\n  }\n\n  /**\n   * Writes 8 bytes in little-endian format, converting them from a <em> signed </em> 64-bit value.\n   *\n   * @param output the output stream where the bytes will be written.\n   * @param value the 64-bit value to convert.\n   * @throws IOException failed to write the value data.\n   */\n  public static void writeUnsigned8Le(ByteBuffer output, long value) throws IOException {\n    Preconditions.checkNotNull(output, \"output == null\");\n\n    ByteOrder order = output.order();\n    output.order(ByteOrder.LITTLE_ENDIAN);\n    output.putLong(value);\n    output.order(order);\n  }\n\n  /**\n   * Writes 4 bytes in little-endian format, converting them from a 32-bit value.\n   *\n   * @param output the output stream where the bytes will be written\n   * @param value the 32-bit value to convert\n   * @throws IOException failed to write the value data\n   */\n  public static void writeUnsigned4Le(ByteBuffer output, long value) throws IOException {\n    Preconditions.checkNotNull(output, \"output == null\");\n    Preconditions.checkArgument(value >= 0, \"value (%s) < 0\", value);\n    Preconditions.checkArgument(\n        value <= 0x00000000ffffffffL, \"value (%s) > 0x00000000ffffffffL\", value);\n\n    output.put((byte) (value & 0xff));\n    output.put((byte) ((value >> 8) & 0xff));\n    output.put((byte) ((value >> 16) & 0xff));\n    output.put((byte) ((value >> 24) & 0xff));\n  }\n\n  /**\n   * Writes 2 bytes in little-endian format, converting them from a 16-bit value.\n   *\n   * @param output the output stream where the bytes will be written\n   * @param value the 16-bit value to convert\n   * @throws IOException failed to write the value data\n   */\n  public static void writeUnsigned2Le(ByteBuffer output, int value) throws IOException {\n    Preconditions.checkNotNull(output, \"output == null\");\n    Preconditions.checkArgument(value >= 0, \"value (%s) < 0\", value);\n    Preconditions.checkArgument(value <= 0x0000ffff, \"value (%s) > 0x0000ffff\", value);\n\n    output.put((byte) (value & 0xff));\n    output.put((byte) ((value >> 8) & 0xff));\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/MsDosDateTimeUtils.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.utils;\n\nimport com.google.common.base.Verify;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/** Yes. This actually refers to MS-DOS in 2015. That's all I have to say about legacy stuff. */\npublic class MsDosDateTimeUtils {\n  /** Utility class: no constructor. */\n  private MsDosDateTimeUtils() {}\n\n  /**\n   * Packs java time value into an MS-DOS time value.\n   *\n   * @param time the time value\n   * @return the MS-DOS packed time\n   */\n  public static int packTime(long time) {\n    Calendar c = Calendar.getInstance();\n    c.setTime(new Date(time));\n\n    int seconds = c.get(Calendar.SECOND);\n    int minutes = c.get(Calendar.MINUTE);\n    int hours = c.get(Calendar.HOUR_OF_DAY);\n\n    /*\n     * Here is how MS-DOS packs a time value:\n     * 0-4: seconds (divided by 2 because we only have 5 bits = 32 different numbers)\n     * 5-10: minutes (6 bits = 64 possible values)\n     * 11-15: hours (5 bits = 32 possible values)\n     *\n     * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx\n     */\n    return (hours << 11) | (minutes << 5) | (seconds / 2);\n  }\n\n  /**\n   * Packs the current time value into an MS-DOS time value.\n   *\n   * @return the MS-DOS packed time\n   */\n  public static int packCurrentTime() {\n    return packTime(new Date().getTime());\n  }\n\n  /**\n   * Packs java time value into an MS-DOS date value.\n   *\n   * @param time the time value\n   * @return the MS-DOS packed date\n   */\n  public static int packDate(long time) {\n    Calendar c = Calendar.getInstance();\n    c.setTime(new Date(time));\n\n    /*\n     * Even MS-DOS used 1 for January. Someone wasn't really thinking when they decided on Java\n     * it would start at 0...\n     */\n    int day = c.get(Calendar.DAY_OF_MONTH);\n    int month = c.get(Calendar.MONTH) + 1;\n\n    /*\n     * MS-DOS counts years starting from 1980. Since its launch date was in 81, it was obviously\n     * not necessary to talk about dates earlier than that.\n     */\n    int year = c.get(Calendar.YEAR) - 1980;\n    Verify.verify(year >= 0 && year < 128);\n\n    /*\n     * Here is how MS-DOS packs a date value:\n     * 0-4: day (5 bits = 32 values)\n     * 5-8: month (4 bits = 16 values)\n     * 9-15: year (7 bits = 128 values)\n     *\n     * source: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx\n     */\n    return (year << 9) | (month << 5) | day;\n  }\n\n  /**\n   * Packs the current time value into an MS-DOS date value.\n   *\n   * @return the MS-DOS packed date\n   */\n  public static int packCurrentDate() {\n    return packDate(new Date().getTime());\n  }\n}\n"
  },
  {
    "path": "apkzlib/src/main/java/com/android/tools/build/apkzlib/zip/utils/RandomAccessFileUtils.java",
    "content": "/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.android.tools.build.apkzlib.zip.utils;\n\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\n\n/** Utility class with utility methods for random access files. */\npublic final class RandomAccessFileUtils {\n\n  private RandomAccessFileUtils() {}\n\n  /**\n   * Reads from an random access file until the provided array is filled. Data is read from the\n   * current position in the file.\n   *\n   * @param raf the file to read data from\n   * @param data the array that will receive the data\n   * @throws IOException failed to read the data\n   */\n  public static void fullyRead(RandomAccessFile raf, byte[] data) throws IOException {\n    int r;\n    int p = 0;\n\n    while ((r = raf.read(data, p, data.length - p)) > 0) {\n      p += r;\n      if (p == data.length) {\n        break;\n      }\n    }\n\n    if (p < data.length) {\n      throw new IOException(\n          \"Failed to read \"\n              + data.length\n              + \" bytes from file. Only \"\n              + p\n              + \" bytes could be read.\");\n    }\n  }\n}\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import com.android.build.api.dsl.ApplicationExtension\nimport com.android.build.api.variant.ApplicationAndroidComponentsExtension\nimport com.android.build.gradle.BaseExtension\nimport org.eclipse.jgit.api.Git\nimport org.eclipse.jgit.internal.storage.file.FileRepository\nimport org.eclipse.jgit.storage.file.FileRepositoryBuilder\n\nplugins {\n    alias(libs.plugins.agp.lib) apply false\n    alias(libs.plugins.agp.app) apply false\n    alias(lspatch.plugins.kotlin.android) apply false\n}\n\nbuildscript {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath(\"org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r\")\n    }\n}\n\nval commitCount = run {\n    val repo = FileRepository(rootProject.file(\".git\"))\n    val refId = repo.refDatabase.exactRef(\"refs/remotes/origin/master\").objectId!!\n    Git(repo).log().add(refId).call().count()\n}\n\nval (coreCommitCount, coreLatestTag) = FileRepositoryBuilder().setGitDir(rootProject.file(\".git/modules/core\"))\n    .runCatching {\n        build().use { repo ->\n            val git = Git(repo)\n            val coreCommitCount =\n                git.log()\n                    .add(repo.refDatabase.exactRef(\"HEAD\").objectId)\n                    .call().count() + 4200\n            val ver = git.describe()\n                .setTags(true)\n                .setAbbrev(0).call().removePrefix(\"v\")\n            coreCommitCount to ver\n        }\n    }.getOrNull() ?: (1 to \"1.0\")\n\n// sync from https://github.com/LSPosed/LSPosed/blob/master/build.gradle.kts\nval defaultManagerPackageName by extra(\"org.lsposed.lspatch\")\nval apiCode by extra(93)\nval verCode by extra(commitCount)\nval verName by extra(\"0.6\")\nval coreVerCode by extra(coreCommitCount)\nval coreVerName by extra(coreLatestTag)\nval androidMinSdkVersion by extra(28)\nval androidTargetSdkVersion by extra(34)\nval androidCompileSdkVersion by extra(34)\nval androidCompileNdkVersion by extra(\"25.2.9519653\")\nval androidBuildToolsVersion by extra(\"34.0.0\")\nval androidSourceCompatibility by extra(JavaVersion.VERSION_17)\nval androidTargetCompatibility by extra(JavaVersion.VERSION_17)\n\ntasks.register<Delete>(\"clean\") {\n    delete(rootProject.buildDir)\n}\n\nlistOf(\"Debug\", \"Release\").forEach { variant ->\n    tasks.register(\"build$variant\") {\n        description = \"Build LSPatch with $variant\"\n        dependsOn(projects.jar.dependencyProject.tasks[\"build$variant\"])\n        dependsOn(projects.manager.dependencyProject.tasks[\"build$variant\"])\n    }\n}\n\ntasks.register(\"buildAll\") {\n    dependsOn(\"buildDebug\", \"buildRelease\")\n}\n\nfun Project.configureBaseExtension() {\n    extensions.findByType(BaseExtension::class)?.run {\n        compileSdkVersion(androidCompileSdkVersion)\n        ndkVersion = androidCompileNdkVersion\n        buildToolsVersion = androidBuildToolsVersion\n\n        externalNativeBuild.cmake {\n            version = \"3.22.1+\"\n        }\n\n        defaultConfig {\n            minSdk = androidMinSdkVersion\n            targetSdk = androidTargetSdkVersion\n            versionCode = verCode\n            versionName = verName\n\n            signingConfigs.create(\"config\") {\n                val androidStoreFile = project.findProperty(\"androidStoreFile\") as String?\n                if (!androidStoreFile.isNullOrEmpty()) {\n                    storeFile = rootProject.file(androidStoreFile)\n                    storePassword = project.property(\"androidStorePassword\") as String\n                    keyAlias = project.property(\"androidKeyAlias\") as String\n                    keyPassword = project.property(\"androidKeyPassword\") as String\n                }\n            }\n\n            externalNativeBuild {\n                cmake {\n                    arguments += \"-DEXTERNAL_ROOT=${File(rootDir.absolutePath, \"core/external\")}\"\n                    arguments += \"-DCORE_ROOT=${File(rootDir.absolutePath, \"core/core/src/main/jni\")}\"\n                    abiFilters(\"arm64-v8a\", \"armeabi-v7a\", \"x86\", \"x86_64\")\n                    val flags = arrayOf(\n                        \"-Wall\",\n                        \"-Qunused-arguments\",\n                        \"-Wno-gnu-string-literal-operator-template\",\n                        \"-fno-rtti\",\n                        \"-fvisibility=hidden\",\n                        \"-fvisibility-inlines-hidden\",\n                        \"-fno-exceptions\",\n                        \"-fno-stack-protector\",\n                        \"-fomit-frame-pointer\",\n                        \"-Wno-builtin-macro-redefined\",\n                        \"-Wno-unused-value\",\n                        \"-D__FILE__=__FILE_NAME__\",\n                    )\n                    cppFlags(\"-std=c++20\", *flags)\n                    cFlags(\"-std=c18\", *flags)\n                    arguments(\n                        \"-DANDROID_STL=none\",\n                        \"-DVERSION_CODE=$verCode\",\n                        \"-DVERSION_NAME=$verName\",\n                    )\n                }\n            }\n        }\n\n        compileOptions {\n            targetCompatibility(androidTargetCompatibility)\n            sourceCompatibility(androidSourceCompatibility)\n        }\n\n        buildTypes {\n            all {\n                signingConfig = if (signingConfigs[\"config\"].storeFile != null) signingConfigs[\"config\"] else signingConfigs[\"debug\"]\n            }\n            named(\"debug\") {\n                externalNativeBuild {\n                    cmake {\n                        arguments.addAll(\n                            arrayOf(\n                                \"-DCMAKE_CXX_FLAGS_DEBUG=-Og\",\n                                \"-DCMAKE_C_FLAGS_DEBUG=-Og\",\n                            )\n                        )\n                    }\n                }\n            }\n            named(\"release\") {\n                externalNativeBuild {\n                    cmake {\n                        val flags = arrayOf(\n                            \"-Wl,--exclude-libs,ALL\",\n                            \"-ffunction-sections\",\n                            \"-fdata-sections\",\n                            \"-Wl,--gc-sections\",\n                            \"-fno-unwind-tables\",\n                            \"-fno-asynchronous-unwind-tables\",\n                            \"-flto=thin\",\n                            \"-Wl,--thinlto-cache-policy,cache_size_bytes=300m\",\n                            \"-Wl,--thinlto-cache-dir=${buildDir.absolutePath}/.lto-cache\",\n                        )\n                        cppFlags.addAll(flags)\n                        cFlags.addAll(flags)\n                        val configFlags = arrayOf(\n                            \"-Oz\",\n                            \"-DNDEBUG\"\n                        ).joinToString(\" \")\n                        arguments.addAll(\n                            arrayOf(\n                                \"-DCMAKE_CXX_FLAGS_RELEASE=$configFlags\",\n                                \"-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=$configFlags\",\n                                \"-DCMAKE_C_FLAGS_RELEASE=$configFlags\",\n                                \"-DCMAKE_C_FLAGS_RELWITHDEBINFO=$configFlags\",\n                                \"-DDEBUG_SYMBOLS_PATH=${buildDir.absolutePath}/symbols\",\n                            )\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    extensions.findByType(ApplicationExtension::class)?.lint {\n        abortOnError = true\n        checkReleaseBuilds = false\n    }\n\n    extensions.findByType(ApplicationAndroidComponentsExtension::class)?.let { androidComponents ->\n        val optimizeReleaseRes = task(\"optimizeReleaseRes\").doLast {\n            val aapt2 = File(\n                androidComponents.sdkComponents.sdkDirectory.get().asFile,\n                \"build-tools/${androidBuildToolsVersion}/aapt2\"\n            )\n            val zip = java.nio.file.Paths.get(\n                project.buildDir.path,\n                \"intermediates\",\n                \"optimized_processed_res\",\n                \"release\",\n                \"resources-release-optimize.ap_\"\n            )\n            val optimized = File(\"${zip}.opt\")\n            val cmd = exec {\n                commandLine(\n                    aapt2, \"optimize\",\n                    \"--collapse-resource-names\",\n                    \"--enable-sparse-encoding\",\n                    \"-o\", optimized,\n                    zip\n                )\n                isIgnoreExitValue = false\n            }\n            if (cmd.exitValue == 0) {\n                delete(zip)\n                optimized.renameTo(zip.toFile())\n            }\n        }\n\n        tasks.configureEach {\n            if (name == \"optimizeReleaseResources\") {\n                finalizedBy(optimizeReleaseRes)\n            }\n        }\n    }\n}\n\nsubprojects {\n    plugins.withId(\"com.android.application\") {\n        configureBaseExtension()\n    }\n    plugins.withId(\"com.android.library\") {\n        configureBaseExtension()\n    }\n}\n"
  },
  {
    "path": "crowdin.yml",
    "content": "#\n# Your Crowdin credentials\n#\n\"project_id_env\": \"CROWDIN_PROJECT_ID\"\n\"api_token_env\": \"CROWDIN_API_TOKEN\"\n\"base_path\": \".\"\n\"base_url\": \"https://lsposed.crowdin.com/api/v2\"\n\"pull_request_title\": \"[translation] Update translation from Crowdin\"\n#\n# Choose file structure in Crowdin\n# e.g. true or false\n#\n\"preserve_hierarchy\": true\n\n#\n# Files configuration\n#\nfiles: [\n  {\n    \"source\": \"/manager/src/main/res/values/strings.xml\",\n    \"translation\": \"/manager/src/main/res/values-%two_letters_code%/%original_file_name%\",\n    \"type\": \"android\",\n    \"dest\": \"/manager/strings.xml\",\n    \"skip_untranslated_strings\": true\n  }\n]\n"
  },
  {
    "path": "gradle/lspatch.versions.toml",
    "content": "[versions]\nroom = \"2.5.2\"\naccompanist = \"0.27.0\"\ncompose-destinations = \"1.9.42-beta\"\nshizuku = \"13.1.2\"\nhiddenapi-refine = \"4.3.0\"\nhiddenapi-stub = \"4.2.0\"\ncompose-bom = \"2023.06.01\"\nkotlin = \"1.8.21\"\nksp = \"1.8.21-1.0.11\"\ncommons-io = \"2.13.0\"\nbeust-jcommander = \"1.82\"\ngoogle-gson = \"2.10.1\"\n\n[plugins]\ngoogle-devtools-ksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nrikka-tools-refine = { id = \"dev.rikka.tools.refine\", version.ref = \"hiddenapi-refine\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\n\n[libraries]\nandroidx-customview = \"androidx.customview:customview:1.2.0-alpha02\"\nandroidx-customview-poolingcontainer = \"androidx.customview:customview-poolingcontainer:1.0.0\"\n\nandroidx-compose-bom = { module = \"androidx.compose:compose-bom\", version.ref = \"compose-bom\" }\nandroidx-compose-ui = { module = \"androidx.compose.ui:ui\" }\nandroidx-compose-ui-tooling = { module = \"androidx.compose.ui:ui-tooling\" }\nandroidx-compose-ui-tooling-preview = { module = \"androidx.compose.ui:ui-tooling-preview\" }\nandroidx-compose-material-icons-extended = { module = \"androidx.compose.material:material-icons-extended\" }\nandroidx-compose-material3 = { module = \"androidx.compose.material3:material3\" }\n\nandroidx-navigation-compose = \"androidx.navigation:navigation-compose:2.6.0\"\n\nandroidx-lifecycle-viewmodel-compose = \"androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1\"\n\nandroidx-activity-compose = \"androidx.activity:activity-compose:1.7.2\"\n\nandroidx-core-ktx = \"androidx.core:core-ktx:1.10.1\"\n\nandroidx-room-ktx = { module = \"androidx.room:room-ktx\", version.ref = \"room\" }\nandroidx-room-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"room\" }\nandroidx-room-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"room\" }\n\ngoogle-accompanist-navigation-animation = { module = \"com.google.accompanist:accompanist-navigation-animation\", version.ref = \"accompanist\" }\ngoogle-accompanist-pager = { module = \"com.google.accompanist:accompanist-pager\", version.ref = \"accompanist\" }\ngoogle-accompanist-swiperefresh = { module = \"com.google.accompanist:accompanist-swiperefresh\", version.ref = \"accompanist\" }\n\nrikka-shizuku-api = { module = \"dev.rikka.shizuku:api\", version.ref = \"shizuku\" }\nrikka-shizuku-provider = { module = \"dev.rikka.shizuku:provider\", version.ref = \"shizuku\" }\n\nrikka-refine = { module = \"dev.rikka.tools.refine:runtime\", version.ref = \"hiddenapi-refine\" }\n\nrikka-hidden-stub = { module = \"dev.rikka.hidden:stub\", version.ref = \"hiddenapi-stub\" }\n\nraamcosta-compose-destinations = { module = \"io.github.raamcosta.compose-destinations:core\", version.ref = \"compose-destinations\" }\nraamcosta-compose-destinations-ksp = { module = \"io.github.raamcosta.compose-destinations:ksp\", version.ref = \"compose-destinations\" }\n\ncommons-io = { module = \"commons-io:commons-io\", version.ref = \"commons-io\" }\n\nbeust-jcommander = { module = \"com.beust:jcommander\", version.ref = \"beust-jcommander\" }\n\ngoogle-gson = { module = \"com.google.code.gson:gson\", version.ref = \"google-gson\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.1.1-bin.zip\nnetworkTimeout=10000\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "android.experimental.enableNewResourceShrinker.preciseShrinking=true\nandroid.enableAppCompileTimeRClass=true\nandroid.useAndroidX=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/HEAD/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\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\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        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    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\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $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# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "jar/.gitignore",
    "content": "/build"
  },
  {
    "path": "jar/build.gradle.kts",
    "content": "val verCode: Int by rootProject.extra\nval verName: String by rootProject.extra\nval androidSourceCompatibility: JavaVersion by rootProject.extra\nval androidTargetCompatibility: JavaVersion by rootProject.extra\n\nplugins {\n    id(\"java-library\")\n}\n\njava {\n    sourceCompatibility = androidSourceCompatibility\n    targetCompatibility = androidTargetCompatibility\n}\n\ndependencies {\n    implementation(projects.patch)\n}\n\nfun Jar.configure(variant: String) {\n    archiveBaseName.set(\"jar-v$verName-$verCode-$variant\")\n    destinationDirectory.set(file(\"${rootProject.projectDir}/out/$variant\"))\n    manifest {\n        attributes(\"Main-Class\" to \"org.lsposed.patch.LSPatch\")\n    }\n    dependsOn(configurations.runtimeClasspath)\n    from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })\n\n    into(\"assets\") {\n        from(\"src/main/assets\")\n        from(\"${rootProject.projectDir}/out/assets/$variant\")\n    }\n\n    exclude(\"META-INF/*.SF\", \"META-INF/*.DSA\", \"META-INF/*.RSA\", \"META-INF/*.MF\", \"META-INF/*.txt\", \"META-INF/versions/**\")\n}\n\ntasks.register<Jar>(\"buildDebug\") {\n    dependsOn(\":meta-loader:copyDebug\")\n    dependsOn(\":patch-loader:copyDebug\")\n    configure(\"debug\")\n}\n\ntasks.register<Jar>(\"buildRelease\") {\n    dependsOn(\":meta-loader:copyRelease\")\n    dependsOn(\":patch-loader:copyRelease\")\n    configure(\"release\")\n}\n"
  },
  {
    "path": "manager/.gitignore",
    "content": "/build"
  },
  {
    "path": "manager/build.gradle.kts",
    "content": "import java.util.Locale\n\nval defaultManagerPackageName: String by rootProject.extra\nval apiCode: Int by rootProject.extra\nval verCode: Int by rootProject.extra\nval verName: String by rootProject.extra\nval coreVerCode: Int by rootProject.extra\nval coreVerName: String by rootProject.extra\n\nplugins {\n    alias(libs.plugins.agp.app)\n    alias(lspatch.plugins.google.devtools.ksp)\n    alias(lspatch.plugins.rikka.tools.refine)\n    alias(lspatch.plugins.kotlin.android)\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    defaultConfig {\n        applicationId = defaultManagerPackageName\n    }\n\n    androidResources {\n        noCompress.add(\".so\")\n    }\n\n    buildTypes {\n        debug {\n            isMinifyEnabled = true\n            proguardFiles(\"proguard-rules-debug.pro\")\n        }\n        release {\n            isMinifyEnabled = true\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        all {\n            sourceSets[name].assets.srcDirs(rootProject.projectDir.resolve(\"out/assets/$name\"))\n        }\n    }\n\n    kotlinOptions {\n        jvmTarget = \"17\"\n    }\n\n    kotlin {\n        jvmToolchain(17)\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n\n    composeOptions {\n        kotlinCompilerExtensionVersion = \"1.4.7\"\n    }\n\n    namespace = \"org.lsposed.lspatch\"\n\n    applicationVariants.all {\n        kotlin.sourceSets {\n            getByName(name) {\n                kotlin.srcDir(\"build/generated/ksp/$name/kotlin\")\n            }\n        }\n    }\n}\n\nafterEvaluate {\n    android.applicationVariants.forEach { variant ->\n        val variantLowered = variant.name.lowercase()\n        val variantCapped = variant.name.replaceFirstChar { it.uppercase() }\n\n        task<Copy>(\"copy${variantCapped}Assets\") {\n            dependsOn(\":meta-loader:copy$variantCapped\")\n            dependsOn(\":patch-loader:copy$variantCapped\")\n            tasks[\"merge${variantCapped}Assets\"].dependsOn(this)\n\n            into(\"$buildDir/intermediates/assets/$variantLowered/merge${variantCapped}Assets\")\n            from(\"${rootProject.projectDir}/out/assets/${variant.name}\")\n        }\n\n        task<Copy>(\"build$variantCapped\") {\n            dependsOn(tasks[\"assemble$variantCapped\"])\n            from(variant.outputs.map { it.outputFile })\n            into(\"${rootProject.projectDir}/out/$variantLowered\")\n            rename(\".*.apk\", \"manager-v$verName-$verCode-$variantLowered.apk\")\n        }\n    }\n}\n\ndependencies {\n    implementation(projects.patch)\n    implementation(projects.services.daemonService)\n    implementation(projects.share.android)\n    implementation(projects.share.java)\n    implementation(platform(lspatch.androidx.compose.bom))\n\n    annotationProcessor(lspatch.androidx.room.compiler)\n    compileOnly(lspatch.rikka.hidden.stub)\n    debugImplementation(lspatch.androidx.compose.ui.tooling)\n    debugImplementation(lspatch.androidx.customview)\n    debugImplementation(lspatch.androidx.customview.poolingcontainer)\n    implementation(lspatch.androidx.activity.compose)\n    implementation(lspatch.androidx.compose.material.icons.extended)\n    implementation(lspatch.androidx.compose.material3)\n    implementation(lspatch.androidx.compose.ui)\n    implementation(lspatch.androidx.compose.ui.tooling.preview)\n    implementation(lspatch.androidx.core.ktx)\n    implementation(lspatch.androidx.lifecycle.viewmodel.compose)\n    implementation(lspatch.androidx.navigation.compose)\n    implementation(libs.androidx.preference)\n    implementation(lspatch.androidx.room.ktx)\n    implementation(lspatch.androidx.room.runtime)\n    implementation(lspatch.google.accompanist.navigation.animation)\n    implementation(lspatch.google.accompanist.pager)\n    implementation(lspatch.google.accompanist.swiperefresh)\n    implementation(libs.material)\n    implementation(libs.gson)\n    implementation(lspatch.rikka.shizuku.api)\n    implementation(lspatch.rikka.shizuku.provider)\n    implementation(lspatch.rikka.refine)\n    implementation(lspatch.raamcosta.compose.destinations)\n    implementation(libs.appiconloader)\n    implementation(libs.hiddenapibypass)\n    ksp(lspatch.androidx.room.compiler)\n    ksp(lspatch.raamcosta.compose.destinations.ksp)\n}\n"
  },
  {
    "path": "manager/proguard-rules-debug.pro",
    "content": "-dontobfuscate\n-keep class com.beust.jcommander.** { *; }\n-keep class org.lsposed.lspatch.Patcher$Options { *; }\n-keep class org.lsposed.lspatch.share.LSPConfig { *; }\n-keep class org.lsposed.lspatch.share.PatchConfig { *; }\n-keepclassmembers class org.lsposed.patch.LSPatch {\n    private <fields>;\n}\n-dontwarn com.google.auto.value.AutoValue$Builder\n-dontwarn com.google.auto.value.AutoValue\n"
  },
  {
    "path": "manager/proguard-rules.pro",
    "content": "-assumenosideeffects class kotlin.jvm.internal.Intrinsics {\n public static void check*(...);\n public static void throw*(...);\n}\n-assumenosideeffects class java.util.Objects {\n    public static ** requireNonNull(...);\n}\n-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {\n   private static ** getDebugMetadataAnnotation(...) return null;\n}\n\n-keep class com.beust.jcommander.** { *; }\n-keep class org.lsposed.lspatch.Patcher$Options { *; }\n-keep class org.lsposed.lspatch.share.LSPConfig { *; }\n-keep class org.lsposed.lspatch.share.PatchConfig { *; }\n-keepclassmembers class org.lsposed.patch.LSPatch {\n    private <fields>;\n}\n-dontwarn com.google.auto.value.AutoValue$Builder\n-dontwarn com.google.auto.value.AutoValue\n"
  },
  {
    "path": "manager/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\n    <uses-permission\n        android:name=\"android.permission.QUERY_ALL_PACKAGES\"\n        tools:ignore=\"QueryAllPackagesPermission\" />\n    <uses-permission android:name=\"android.permission.REQUEST_DELETE_PACKAGES\" />\n\n    <application\n        android:name=\".LSPApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.Material3.DayNight.NoActionBar\">\n\n        <activity\n            android:name=\".ui.activity.MainActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:scheme=\"file\" />\n                <data android:scheme=\"content\" />\n                <data android:mimeType=\"application/vnd.android.package-archive\" />\n            </intent-filter>\n        </activity>\n\n        <service\n            android:name=\".manager.ModuleService\"\n            android:exported=\"true\" />\n\n        <provider\n            android:name=\"rikka.shizuku.ShizukuProvider\"\n            android:authorities=\"org.lsposed.lspatch.shizuku\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            android:multiprocess=\"false\"\n            android:permission=\"android.permission.INTERACT_ACROSS_USERS_FULL\" />\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt",
    "content": "package org.lsposed.lspatch\n\nimport android.app.Application\nimport android.content.Context\nimport android.content.SharedPreferences\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.lsposed.hiddenapibypass.HiddenApiBypass\nimport org.lsposed.lspatch.manager.AppBroadcastReceiver\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.ShizukuApi\nimport java.io.File\n\nlateinit var lspApp: LSPApplication\n\nclass LSPApplication : Application() {\n\n    lateinit var prefs: SharedPreferences\n    lateinit var tmpApkDir: File\n\n    val globalScope = CoroutineScope(Dispatchers.Default)\n\n    override fun onCreate() {\n        super.onCreate()\n        HiddenApiBypass.addHiddenApiExemptions(\"\")\n        lspApp = this\n        filesDir.mkdir()\n        tmpApkDir = cacheDir.resolve(\"apk\").also { it.mkdir() }\n        prefs = lspApp.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\n        ShizukuApi.init()\n        AppBroadcastReceiver.register(this)\n        globalScope.launch { LSPPackageManager.fetchAppList() }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/Patcher.kt",
    "content": "package org.lsposed.lspatch\n\nimport androidx.core.net.toUri\nimport androidx.documentfile.provider.DocumentFile\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.lsposed.lspatch.config.Configs\nimport org.lsposed.lspatch.config.MyKeyStore\nimport org.lsposed.lspatch.share.Constants\nimport org.lsposed.lspatch.share.PatchConfig\nimport org.lsposed.patch.LSPatch\nimport org.lsposed.patch.util.Logger\nimport java.io.IOException\n\nobject Patcher {\n\n    class Options(\n        private val config: PatchConfig,\n        private val apkPaths: List<String>,\n        private val embeddedModules: List<String>?\n    ) {\n        fun toStringArray(): Array<String> {\n            return buildList {\n                addAll(apkPaths)\n                add(\"-o\"); add(lspApp.tmpApkDir.absolutePath)\n                if (config.debuggable) add(\"-d\")\n                add(\"-l\"); add(config.sigBypassLevel.toString())\n                if (config.useManager) add(\"--manager\")\n                if (config.overrideVersionCode) add(\"-r\")\n                if (Configs.detailPatchLogs) add(\"-v\")\n                embeddedModules?.forEach {\n                    add(\"-m\"); add(it)\n                }\n                if (!MyKeyStore.useDefault) {\n                    addAll(arrayOf(\"-k\", MyKeyStore.file.path, Configs.keyStorePassword, Configs.keyStoreAlias, Configs.keyStoreAliasPassword))\n                }\n            }.toTypedArray()\n        }\n    }\n\n    suspend fun patch(logger: Logger, options: Options) {\n        withContext(Dispatchers.IO) {\n            LSPatch(logger, *options.toStringArray()).doCommandLine()\n\n            val uri = Configs.storageDirectory?.toUri()\n                ?: throw IOException(\"Uri is null\")\n            val root = DocumentFile.fromTreeUri(lspApp, uri)\n                ?: throw IOException(\"DocumentFile is null\")\n            root.listFiles().forEach {\n                if (it.name?.endsWith(Constants.PATCH_FILE_SUFFIX) == true) it.delete()\n            }\n            lspApp.tmpApkDir.walk()\n                .filter { it.name.endsWith(Constants.PATCH_FILE_SUFFIX) }\n                .forEach { apk ->\n                    val file = root.createFile(\"application/vnd.android.package-archive\", apk.name)\n                        ?: throw IOException(\"Failed to create output file\")\n                    val output = lspApp.contentResolver.openOutputStream(file.uri)\n                        ?: throw IOException(\"Failed to open output stream\")\n                    output.use {\n                        apk.inputStream().use { input ->\n                            input.copyTo(output)\n                        }\n                    }\n                }\n            logger.i(\"Patched files are saved to ${root.uri.lastPathSegment}\")\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/config/ConfigManager.kt",
    "content": "package org.lsposed.lspatch.config\n\nimport android.content.pm.PackageManager\nimport android.util.Log\nimport androidx.room.Room\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.withContext\nimport org.lsposed.lspatch.database.LSPDatabase\nimport org.lsposed.lspatch.database.entity.Module\nimport org.lsposed.lspatch.database.entity.Scope\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.util.ModuleLoader\nimport java.io.File\n\nobject ConfigManager {\n\n    private const val TAG = \"ConfigManager\"\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    private val dispatcher = Dispatchers.Default.limitedParallelism(1)\n\n    private val db: LSPDatabase = Room.databaseBuilder(\n        lspApp, LSPDatabase::class.java, \"modules_config.db\"\n    ).build()\n\n    private val moduleDao = db.moduleDao()\n    private val scopeDao = db.scopeDao()\n\n    private val loadedModules = mutableMapOf<Module, org.lsposed.lspd.models.Module>()\n\n    suspend fun updateModules(newModules: Map<String, String>) =\n        withContext(dispatcher) {\n            for (module in moduleDao.getAll()) {\n                val apkPath = newModules[module.pkgName]\n                if (apkPath == null) {\n                    moduleDao.delete(module)\n                    loadedModules.remove(module)\n                } else if (module.apkPath != apkPath) {\n                    module.apkPath = apkPath\n                    loadedModules.remove(module)\n                }\n            }\n            for ((pkgName, apkPath) in newModules) {\n                moduleDao.insert(Module(pkgName, apkPath))\n            }\n        }\n\n    suspend fun activateModule(pkgName: String, module: Module) =\n        withContext(dispatcher) {\n            scopeDao.insert(Scope(appPkgName = pkgName, modulePkgName = module.pkgName))\n        }\n\n    suspend fun deactivateModule(pkgName: String, module: Module) =\n        withContext(dispatcher) {\n            scopeDao.delete(Scope(appPkgName = pkgName, modulePkgName = module.pkgName))\n        }\n\n    suspend fun getModulesForApp(pkgName: String): List<Module> =\n        withContext(dispatcher) {\n            return@withContext scopeDao.getModulesForApp(pkgName)\n        }\n\n    suspend fun getModuleFilesForApp(pkgName: String): List<org.lsposed.lspd.models.Module> =\n        withContext(dispatcher) {\n            val modules = scopeDao.getModulesForApp(pkgName)\n            return@withContext modules.mapNotNull {\n                if (!File(it.apkPath).exists()) {\n                    loadedModules.remove(it)\n                    try {\n                        it.apkPath = lspApp.packageManager.getApplicationInfo(it.pkgName, 0).sourceDir\n                    } catch (e: PackageManager.NameNotFoundException) {\n                        moduleDao.delete(moduleDao.getModule(it.pkgName))\n                        Log.w(TAG, \"Module may be uninstalled: ${it.pkgName}\")\n                        return@mapNotNull null\n                    }\n                    Log.i(TAG, \"Module apk path updated: ${it.pkgName}\")\n                }\n                loadedModules.getOrPut(it) {\n                    org.lsposed.lspd.models.Module().apply {\n                        packageName = it.pkgName\n                        apkPath = it.apkPath\n                        file = ModuleLoader.loadModule(it.apkPath)\n                    }\n                }\n            }\n        }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/config/Configs.kt",
    "content": "package org.lsposed.lspatch.config\n\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.ui.util.delegateStateOf\nimport org.lsposed.lspatch.ui.util.getValue\nimport org.lsposed.lspatch.ui.util.setValue\n\nobject Configs {\n\n    private const val PREFS_KEYSTORE_PASSWORD = \"keystore_password\"\n    private const val PREFS_KEYSTORE_ALIAS = \"keystore_alias\"\n    private const val PREFS_KEYSTORE_ALIAS_PASSWORD = \"keystore_alias_password\"\n    private const val PREFS_STORAGE_DIRECTORY = \"storage_directory\"\n    private const val PREFS_DETAIL_PATCH_LOGS = \"detail_patch_logs\"\n\n    var keyStorePassword by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_PASSWORD, \"123456\")!!) {\n        lspApp.prefs.edit().putString(PREFS_KEYSTORE_PASSWORD, it).apply()\n    }\n\n    var keyStoreAlias by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_ALIAS, \"key0\")!!) {\n        lspApp.prefs.edit().putString(PREFS_KEYSTORE_ALIAS, it).apply()\n    }\n\n    var keyStoreAliasPassword by delegateStateOf(lspApp.prefs.getString(PREFS_KEYSTORE_ALIAS_PASSWORD, \"123456\")!!) {\n        lspApp.prefs.edit().putString(PREFS_KEYSTORE_ALIAS_PASSWORD, it).apply()\n    }\n\n    var storageDirectory by delegateStateOf(lspApp.prefs.getString(PREFS_STORAGE_DIRECTORY, null)) {\n        lspApp.prefs.edit().putString(PREFS_STORAGE_DIRECTORY, it).apply()\n    }\n\n    var detailPatchLogs by delegateStateOf(lspApp.prefs.getBoolean(PREFS_DETAIL_PATCH_LOGS, true)) {\n        lspApp.prefs.edit().putBoolean(PREFS_DETAIL_PATCH_LOGS, it).apply()\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt",
    "content": "package org.lsposed.lspatch.config\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.lsposed.lspatch.lspApp\nimport java.io.File\n\nobject MyKeyStore {\n\n    val file = File(\"${lspApp.filesDir}/keystore.bks\")\n    val tmpFile = File(\"${lspApp.filesDir}/keystore.bks.tmp\")\n\n    var useDefault by mutableStateOf(!file.exists())\n        private set\n\n    suspend fun reset() {\n        withContext(Dispatchers.IO) {\n            file.delete()\n            Configs.keyStorePassword = \"123456\"\n            Configs.keyStoreAlias = \"key0\"\n            Configs.keyStoreAliasPassword = \"123456\"\n            useDefault = true\n        }\n    }\n\n    suspend fun setCustom(password: String, alias: String, aliasPassword: String) {\n        withContext(Dispatchers.IO) {\n            tmpFile.renameTo(file)\n            Configs.keyStorePassword = password\n            Configs.keyStoreAlias = alias\n            Configs.keyStoreAliasPassword = aliasPassword\n            useDefault = false\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/database/LSPDatabase.kt",
    "content": "package org.lsposed.lspatch.database\n\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\nimport org.lsposed.lspatch.database.dao.ModuleDao\nimport org.lsposed.lspatch.database.dao.ScopeDao\n\nimport org.lsposed.lspatch.database.entity.Module\nimport org.lsposed.lspatch.database.entity.Scope\n\n@Database(entities = [Module::class, Scope::class], version = 1)\nabstract class LSPDatabase : RoomDatabase() {\n    abstract fun moduleDao(): ModuleDao\n    abstract fun scopeDao(): ScopeDao\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/database/dao/ModuleDao.kt",
    "content": "package org.lsposed.lspatch.database.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport org.lsposed.lspatch.database.entity.Module\n\n@Dao\ninterface ModuleDao {\n\n    @Query(\"SELECT * FROM module WHERE pkgName = :pkgName\")\n    suspend fun getModule(pkgName: String): Module\n\n    @Query(\"SELECT * FROM module\")\n    suspend fun getAll(): List<Module>\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    suspend fun insert(module: Module)\n\n    @Delete\n    suspend fun delete(module: Module)\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/database/dao/ScopeDao.kt",
    "content": "package org.lsposed.lspatch.database.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport org.lsposed.lspatch.database.entity.Module\nimport org.lsposed.lspatch.database.entity.Scope\n\n@Dao\ninterface ScopeDao {\n\n    @Query(\"SELECT * FROM module INNER JOIN scope ON module.pkgName = scope.modulePkgName WHERE scope.appPkgName = :appPkgName\")\n    suspend fun getModulesForApp(appPkgName: String): List<Module>\n\n    @Insert\n    suspend fun insert(scope: Scope)\n\n    @Delete\n    suspend fun delete(scope: Scope)\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/database/entity/Module.kt",
    "content": "package org.lsposed.lspatch.database.entity\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity\ndata class Module(\n    @PrimaryKey val pkgName: String,\n    var apkPath: String\n)\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/database/entity/Scope.kt",
    "content": "package org.lsposed.lspatch.database.entity\n\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\n\n@Entity(\n    primaryKeys = [\"appPkgName\", \"modulePkgName\"],\n    foreignKeys = [ForeignKey(entity = Module::class, parentColumns = [\"pkgName\"], childColumns = [\"modulePkgName\"], onDelete = ForeignKey.CASCADE)]\n)\ndata class Scope(\n    val appPkgName: String,\n    val modulePkgName: String\n)\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/manager/AppBroadcastReceiver.kt",
    "content": "package org.lsposed.lspatch.manager\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.util.Log\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.util.LSPPackageManager\n\nclass AppBroadcastReceiver : BroadcastReceiver() {\n\n    companion object {\n        private const val TAG = \"AppBroadcastReceiver\"\n\n        private val actions = setOf(\n            Intent.ACTION_PACKAGE_ADDED,\n            Intent.ACTION_PACKAGE_REMOVED,\n            Intent.ACTION_PACKAGE_REPLACED\n        )\n\n        fun register(context: Context) {\n            val filter = IntentFilter().apply {\n                actions.forEach(::addAction)\n                addDataScheme(\"package\")\n            }\n            context.registerReceiver(AppBroadcastReceiver(), filter)\n        }\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        if (intent.action in actions) {\n            lspApp.globalScope.launch {\n                Log.i(TAG, \"Received intent: $intent\")\n                LSPPackageManager.fetchAppList()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/manager/ManagerService.kt",
    "content": "package org.lsposed.lspatch.manager\n\nimport android.os.Binder\nimport android.os.Bundle\nimport android.os.IBinder\nimport android.os.ParcelFileDescriptor\nimport android.util.Log\nimport kotlinx.coroutines.runBlocking\nimport org.lsposed.lspatch.config.ConfigManager\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspd.models.Module\nimport org.lsposed.lspd.service.ILSPApplicationService\n\nobject ManagerService : ILSPApplicationService.Stub() {\n\n    private const val TAG = \"ManagerService\"\n\n    override fun getLegacyModulesList(): List<Module> {\n        val app = lspApp.packageManager.getNameForUid(Binder.getCallingUid())\n        val list = app?.let {\n            runBlocking { ConfigManager.getModuleFilesForApp(it) }\n        }.orEmpty()\n        Log.d(TAG, \"$app calls getLegacyModulesList: $list\")\n        return list\n    }\n\n    override fun getModulesList(): List<Module> {\n        return emptyList()\n    }\n\n    override fun getPrefsPath(packageName: String): String {\n        TODO(\"Not yet implemented\")\n    }\n\n    override fun requestInjectedManagerBinder(binder: List<IBinder>?): ParcelFileDescriptor? {\n        return null\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/manager/ModuleService.kt",
    "content": "package org.lsposed.lspatch.manager\n\nimport android.app.Service\nimport android.content.Intent\nimport android.os.IBinder\nimport android.util.Log\n\n\nclass ModuleService : Service() {\n\n    companion object {\n        private const val TAG = \"ModuleService\"\n    }\n\n    override fun onBind(intent: Intent): IBinder? {\n        val packageName = intent.getStringExtra(\"packageName\") ?: return null\n        // TODO: Authentication\n        Log.i(TAG, \"$packageName requests binder\")\n        return ManagerService.asBinder()\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/activity/MainActivity.kt",
    "content": "package org.lsposed.lspatch.ui.activity\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.animation.ExperimentalAnimationApi\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavGraph.Companion.findStartDestination\nimport androidx.navigation.NavHostController\nimport com.google.accompanist.navigation.animation.rememberAnimatedNavController\nimport com.ramcosta.composedestinations.DestinationsNavHost\nimport org.lsposed.lspatch.ui.page.BottomBarDestination\nimport org.lsposed.lspatch.ui.page.NavGraphs\nimport org.lsposed.lspatch.ui.page.appCurrentDestinationAsState\nimport org.lsposed.lspatch.ui.page.destinations.Destination\nimport org.lsposed.lspatch.ui.page.startAppDestination\nimport org.lsposed.lspatch.ui.theme.LSPTheme\nimport org.lsposed.lspatch.ui.util.LocalSnackbarHost\n\nclass MainActivity : ComponentActivity() {\n\n    @OptIn(ExperimentalAnimationApi::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            val navController = rememberAnimatedNavController()\n            LSPTheme {\n                val snackbarHostState = remember { SnackbarHostState() }\n                CompositionLocalProvider(LocalSnackbarHost provides snackbarHostState) {\n                    Scaffold(\n                        bottomBar = { BottomBar(navController) },\n                        snackbarHost = { SnackbarHost(snackbarHostState) }\n                    ) { innerPadding ->\n                        DestinationsNavHost(\n                            modifier = Modifier.padding(innerPadding),\n                            navGraph = NavGraphs.root,\n                            navController = navController\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun BottomBar(navController: NavHostController) {\n    val currentDestination: Destination = navController.appCurrentDestinationAsState().value\n        ?: NavGraphs.root.startAppDestination\n    var topDestination by rememberSaveable { mutableStateOf(currentDestination.route) }\n    LaunchedEffect(currentDestination) {\n        val queue = navController.currentBackStack.value\n        if (queue.size == 2) topDestination = queue[1].destination.route!!\n        else if (queue.size > 2) topDestination = queue[2].destination.route!!\n    }\n\n    NavigationBar(tonalElevation = 8.dp) {\n        BottomBarDestination.values().forEach { destination ->\n            NavigationBarItem(\n                selected = topDestination == destination.direction.route,\n                onClick = {\n                    navController.navigate(destination.direction.route) {\n                        popUpTo(navController.graph.findStartDestination().id) {\n                            saveState = true\n                        }\n                        launchSingleTop = true\n                        restoreState = true\n                    }\n                },\n                icon = {\n                    if (topDestination == destination.direction.route) Icon(destination.iconSelected, stringResource(destination.label))\n                    else Icon(destination.iconNotSelected, stringResource(destination.label))\n                },\n                label = { Text(stringResource(destination.label)) },\n                alwaysShowLabel = false\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/AnywhereDropdown.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.PressInteraction\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.DpOffset\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun AnywhereDropdown(\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    expanded: Boolean,\n    onDismissRequest: () -> Unit,\n    onClick: () -> Unit,\n    onLongClick: (() -> Unit)? = null,\n    surface: @Composable () -> Unit,\n    content: @Composable ColumnScope.() -> Unit\n) {\n    val indication = LocalIndication.current\n    val interactionSource = remember { MutableInteractionSource() }\n    val state by interactionSource.interactions.collectAsState(null)\n    var offset by remember { mutableStateOf(Offset.Zero) }\n    val dpOffset = with(LocalDensity.current) {\n        DpOffset(offset.x.toDp(), offset.y.toDp())\n    }\n\n    LaunchedEffect(state) {\n        if (state is PressInteraction.Press) {\n            val i = state as PressInteraction.Press\n            offset = i.pressPosition\n        }\n        if (state is PressInteraction.Release) {\n            val i = state as PressInteraction.Release\n            offset = i.press.pressPosition\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .combinedClickable(\n                interactionSource = interactionSource,\n                indication = indication,\n                enabled = enabled,\n                onClick = onClick,\n                onLongClick = onLongClick\n            )\n    ) {\n        surface()\n        Box {\n            DropdownMenu(\n                expanded = expanded,\n                onDismissRequest = onDismissRequest,\n                offset = dpOffset,\n                content = content\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/AppItem.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport android.graphics.drawable.GradientDrawable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowForwardIos\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.core.graphics.drawable.toBitmap\nimport org.lsposed.lspatch.ui.theme.LSPTheme\n\n@Composable\nfun AppItem(\n    modifier: Modifier = Modifier,\n    icon: ImageBitmap,\n    label: String,\n    packageName: String,\n    checked: Boolean? = null,\n    rightIcon: (@Composable () -> Unit)? = null,\n    additionalContent: (@Composable ColumnScope.() -> Unit)? = null,\n) {\n    if (checked != null && rightIcon != null)\n        throw IllegalArgumentException(\"`checked` and `rightIcon` should not be both set\")\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(20.dp)\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(20.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                bitmap = icon,\n                contentDescription = label,\n                tint = Color.Unspecified\n            )\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.spacedBy(1.dp)\n            ) {\n                Text(label)\n                Text(\n                    text = packageName,\n                    fontFamily = FontFamily.Monospace,\n                    style = MaterialTheme.typography.bodySmall\n                )\n                additionalContent?.invoke(this)\n            }\n            if (checked != null) {\n                Checkbox(\n                    checked = checked,\n                    onCheckedChange = null,\n                    modifier = Modifier.padding(start = 20.dp)\n                )\n            }\n            if (rightIcon != null) {\n                rightIcon()\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun AppItemPreview() {\n    LSPTheme {\n        val shape = GradientDrawable()\n        shape.shape = GradientDrawable.RECTANGLE\n        shape.setColor(MaterialTheme.colorScheme.primary.toArgb())\n        AppItem(\n            icon = shape.toBitmap().asImageBitmap(),\n            label = \"Sample App\",\n            packageName = \"org.lsposed.sample\",\n            rightIcon = { Icon(Icons.Filled.ArrowForwardIos, null) }\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/CenterTopBar.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport androidx.compose.material3.CenterAlignedTopAppBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport org.lsposed.lspatch.ui.util.SampleStringProvider\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Preview\n@Composable\nfun CenterTopBar(@PreviewParameter(SampleStringProvider::class, 1) text: String) {\n    CenterAlignedTopAppBar(\n        title = {\n            Text(\n                text = text,\n                color = MaterialTheme.colorScheme.primary,\n                fontWeight = FontWeight.Bold,\n                fontFamily = FontFamily.Monospace,\n                style = MaterialTheme.typography.titleMedium\n            )\n        }\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/LoadingDialog.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\n\n@Preview\n@Composable\nfun LoadingDialog() {\n    Dialog(\n        onDismissRequest = {},\n        properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)\n    ) {\n        Box(\n            modifier = Modifier\n                .size(100.dp)\n                .background(Color.White, shape = RoundedCornerShape(8.dp)),\n            contentAlignment = Alignment.Center,\n            content = { CircularProgressIndicator() }\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/SearchBar.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport android.util.Log\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.outlined.ArrowBack\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\nprivate const val TAG = \"SearchBar\"\n\n@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun SearchAppBar(\n    title: @Composable () -> Unit,\n    searchText: String,\n    onSearchTextChange: (String) -> Unit,\n    onClearClick: () -> Unit,\n    onBackClick: () -> Unit,\n    onConfirm: (() -> Unit)? = null\n) {\n    val keyboardController = LocalSoftwareKeyboardController.current\n    val focusRequester = remember { FocusRequester() }\n    var onSearch by remember { mutableStateOf(false) }\n\n    if (onSearch) {\n        LaunchedEffect(Unit) { focusRequester.requestFocus() }\n    }\n    DisposableEffect(Unit) {\n        onDispose {\n            keyboardController?.hide()\n        }\n    }\n\n    TopAppBar(\n        title = {\n            Box {\n                AnimatedVisibility(\n                    modifier = Modifier.align(Alignment.CenterStart),\n                    visible = !onSearch,\n                    enter = fadeIn(),\n                    exit = fadeOut(),\n                    content = { title() }\n                )\n\n                AnimatedVisibility(\n                    visible = onSearch,\n                    enter = fadeIn(),\n                    exit = fadeOut()\n                ) {\n                    OutlinedTextField(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(vertical = 2.dp)\n                            .focusRequester(focusRequester)\n                            .onFocusChanged { focusState ->\n                                if (focusState.isFocused) onSearch = true\n                                Log.d(TAG, \"onFocusChanged: $focusState\")\n                            },\n                        value = searchText,\n                        onValueChange = onSearchTextChange,\n                        trailingIcon = {\n                            IconButton(\n                                onClick = {\n                                    onSearch = false\n                                    keyboardController?.hide()\n                                    onClearClick()\n                                },\n                                content = { Icon(Icons.Filled.Close, null) }\n                            )\n                        },\n                        maxLines = 1,\n                        singleLine = true,\n                        keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),\n                        keyboardActions = KeyboardActions(onDone = {\n                            keyboardController?.hide()\n                            onConfirm?.invoke()\n                        })\n                    )\n                }\n            }\n        },\n        navigationIcon = {\n            IconButton(\n                onClick = onBackClick,\n                content = { Icon(Icons.Outlined.ArrowBack, null) }\n            )\n        },\n        actions = {\n            AnimatedVisibility(\n                visible = !onSearch\n            ) {\n                IconButton(\n                    onClick = { onSearch = true },\n                    content = { Icon(Icons.Filled.Search, null) }\n                )\n            }\n        }\n    )\n}\n\n@Preview\n@Composable\nprivate fun SearchAppBarPreview() {\n    var searchText by remember { mutableStateOf(\"\") }\n    SearchAppBar(\n        title = { Text(\"Search text\") },\n        searchText = searchText,\n        onSearchTextChange = { searchText = it },\n        onClearClick = { searchText = \"\" },\n        onBackClick = {}\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/SelectionColumn.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.unit.dp\n\nobject SelectionColumnScope {\n\n    @Composable\n    fun SelectionItem(\n        modifier: Modifier = Modifier,\n        selected: Boolean,\n        onClick: () -> Unit,\n        icon: ImageVector,\n        title: String,\n        desc: String? = null,\n        extraContent: (@Composable ColumnScope.() -> Unit)? = null\n    ) {\n        Row(\n            modifier = modifier\n                .fillMaxWidth()\n                .heightIn(min = 64.dp)\n                .clip(RoundedCornerShape(4.dp))\n                .background(\n                    animateColorAsState(\n                        if (selected) MaterialTheme.colorScheme.primaryContainer\n                        else MaterialTheme.colorScheme.inverseOnSurface\n                    ).value\n                )\n                .clickable { onClick() }\n                .padding(16.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                imageVector = icon,\n                contentDescription = null,\n                modifier = Modifier.size(24.dp)\n            )\n            Column {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleMedium\n                )\n                if (desc != null || extraContent != null) {\n                    AnimatedVisibility(\n                        visible = selected,\n                        enter = fadeIn() + expandVertically(expandFrom = Alignment.Top),\n                        exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom)\n                    ) {\n                        Column {\n                            if (desc != null) {\n                                Text(\n                                    text = desc,\n                                    modifier = Modifier.padding(top = 8.dp),\n                                    style = MaterialTheme.typography.bodyMedium\n                                )\n                            }\n                            extraContent?.invoke(this)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n\n@Composable\nfun SelectionColumn(\n    modifier: Modifier = Modifier,\n    content: @Composable() (SelectionColumnScope.() -> Unit)\n) {\n    Column(\n        modifier = modifier.clip(RoundedCornerShape(32.dp)),\n        verticalArrangement = Arrangement.spacedBy(2.dp),\n        content = { SelectionColumnScope.content() }\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/Shimmer.kt",
    "content": "package org.lsposed.lspatch.ui.component\n\nimport androidx.compose.animation.core.*\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\nprivate val ShimmerColorShades\n    @Composable get() = listOf(\n        MaterialTheme.colorScheme.secondaryContainer.copy(0.9f),\n        MaterialTheme.colorScheme.secondaryContainer.copy(0.2f),\n        MaterialTheme.colorScheme.secondaryContainer.copy(0.9f)\n    )\n\nclass ShimmerScope(val brush: Brush)\n\n@Composable\nfun ShimmerAnimation(\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    content: @Composable ShimmerScope.() -> Unit\n) {\n    val transition = rememberInfiniteTransition()\n    val translateAnim by transition.animateFloat(\n        initialValue = 0f,\n        targetValue = 1000f,\n        animationSpec = infiniteRepeatable(\n            tween(durationMillis = 1200, easing = FastOutSlowInEasing),\n            RepeatMode.Reverse\n        )\n    )\n\n    val brush = Brush.linearGradient(\n        colors = if (enabled) ShimmerColorShades else List(3) { ShimmerColorShades[0] },\n        start = Offset(10f, 10f),\n        end = Offset(translateAnim, translateAnim)\n    )\n\n    Surface(modifier.background(brush)) {\n        content(ShimmerScope(brush))\n    }\n}\n\n@Preview\n@Composable\nprivate fun ShimmerPreview() {\n    ShimmerAnimation {\n        Column(modifier = Modifier.padding(16.dp)) {\n            Spacer(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .size(250.dp)\n                    .background(brush = brush)\n            )\n            Spacer(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(30.dp)\n                    .padding(vertical = 8.dp)\n                    .background(brush = brush)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/settings/CheckBox.kt",
    "content": "package org.lsposed.lspatch.ui.component.settings\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Api\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.tooling.preview.Preview\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsCheckBox(\n    modifier: Modifier = Modifier,\n    checked: Boolean,\n    enabled: Boolean = true,\n    icon: ImageVector? = null,\n    title: String,\n    desc: String? = null,\n    extraContent: (@Composable ColumnScope.() -> Unit)? = null\n) {\n    SettingsSlot(modifier, enabled, icon, title, desc, extraContent) {\n        Checkbox(checked = checked, onCheckedChange = null)\n    }\n}\n\n@Preview\n@Composable\nprivate fun SettingsCheckBoxPreview() {\n    var checked1 by remember { mutableStateOf(false) }\n    var checked2 by remember { mutableStateOf(false) }\n    Column {\n        SettingsCheckBox(\n            modifier = Modifier.clickable { checked1 = !checked1 },\n            checked = checked1,\n            title = \"Title\",\n            desc = \"Description\"\n        )\n        SettingsCheckBox(\n            modifier = Modifier.clickable { checked2 = !checked2 },\n            checked = checked2,\n            icon = Icons.Outlined.Api,\n            title = \"Title\",\n            desc = \"Description\"\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Slot.kt",
    "content": "package org.lsposed.lspatch.ui.component.settings\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun SettingsSlot(\n    modifier: Modifier,\n    enabled: Boolean,\n    icon: ImageVector? = null,\n    title: String,\n    desc: String?,\n    extraContent: (@Composable ColumnScope.() -> Unit)? = null,\n    action: (@Composable RowScope.() -> Unit)?,\n) {\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .alpha(if (enabled) 1f else 0.5f)\n            .padding(horizontal = 16.dp, vertical = 8.dp),\n        horizontalArrangement = Arrangement.spacedBy(16.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Box(\n            modifier = modifier.size(24.dp),\n            contentAlignment = Alignment.Center,\n        ) {\n            if (icon != null) {\n                Icon(\n                    imageVector = icon,\n                    contentDescription = null,\n                    modifier = Modifier.fillMaxSize()\n                )\n            }\n        }\n        Column(Modifier.weight(1f).padding(vertical = 6.dp)) {\n            Text(text = title, style = MaterialTheme.typography.titleMedium)\n            Column {\n                if (desc != null) {\n                    Text(\n                        text = desc,\n                        style = MaterialTheme.typography.bodyMedium,\n                        modifier = Modifier\n                            .alpha(0.75f)\n                            .padding(top = 4.dp)\n                    )\n                }\n                extraContent?.invoke(this)\n            }\n        }\n        action?.invoke(this)\n    }\n}\n\n@Composable\nfun SettingsItem(\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    icon: ImageVector? = null,\n    title: String,\n    desc: String? = null,\n    extraContent: (@Composable ColumnScope.() -> Unit)? = null\n) = SettingsSlot(modifier, enabled, icon, title, desc, extraContent, null)\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/component/settings/Switch.kt",
    "content": "package org.lsposed.lspatch.ui.component.settings\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Api\nimport androidx.compose.material3.Switch\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.tooling.preview.Preview\n\n@Composable\nfun SettingsSwitch(\n    modifier: Modifier = Modifier,\n    checked: Boolean,\n    enabled: Boolean = true,\n    icon: ImageVector? = null,\n    title: String,\n    desc: String? = null,\n    extraContent: (@Composable ColumnScope.() -> Unit)? = null\n) {\n    SettingsSlot(modifier, enabled, icon, title, desc, extraContent) {\n        Switch(checked = checked, onCheckedChange = null)\n    }\n}\n\n@Preview\n@Composable\nprivate fun SettingsCheckBoxPreview() {\n    var checked1 by remember { mutableStateOf(false) }\n    var checked2 by remember { mutableStateOf(false) }\n    Column {\n        SettingsSwitch(\n            modifier = Modifier.clickable { checked1 = !checked1 },\n            checked = checked1,\n            title = \"Title\",\n            desc = \"Description\"\n        )\n        SettingsSwitch(\n            modifier = Modifier.clickable { checked2 = !checked2 },\n            checked = checked2,\n            icon = Icons.Outlined.Api,\n            title = \"Title\",\n            desc = \"Description\"\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/BottomBarDestination.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport androidx.annotation.StringRes\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.*\nimport androidx.compose.material.icons.outlined.*\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport com.ramcosta.composedestinations.spec.DirectionDestinationSpec\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.ui.page.destinations.*\n\nenum class BottomBarDestination(\n    val direction: DirectionDestinationSpec,\n    @StringRes val label: Int,\n    val iconSelected: ImageVector,\n    val iconNotSelected: ImageVector\n) {\n    Repo(RepoScreenDestination, R.string.screen_repo, Icons.Filled.GetApp, Icons.Outlined.GetApp),\n    Manage(ManageScreenDestination, R.string.screen_manage, Icons.Filled.Dashboard, Icons.Outlined.Dashboard),\n    Home(HomeScreenDestination, R.string.app_name, Icons.Filled.Home, Icons.Outlined.Home),\n    Logs(LogsScreenDestination, R.string.screen_logs, Icons.Filled.Assignment, Icons.Outlined.Assignment),\n    Settings(SettingsScreenDestination, R.string.screen_settings, Icons.Filled.Settings, Icons.Outlined.Settings);\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/HomeScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport android.app.Activity\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.CheckCircle\nimport androidx.compose.material.icons.outlined.Warning\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.ramcosta.composedestinations.annotation.Destination\nimport com.ramcosta.composedestinations.annotation.RootNavGraph\nimport com.ramcosta.composedestinations.navigation.DestinationsNavigator\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.share.LSPConfig\nimport org.lsposed.lspatch.ui.component.CenterTopBar\nimport org.lsposed.lspatch.ui.page.destinations.ManageScreenDestination\nimport org.lsposed.lspatch.ui.page.destinations.NewPatchScreenDestination\nimport org.lsposed.lspatch.ui.util.HtmlText\nimport org.lsposed.lspatch.ui.util.LocalSnackbarHost\nimport org.lsposed.lspatch.util.ShizukuApi\nimport rikka.shizuku.Shizuku\n\n@OptIn(ExperimentalMaterial3Api::class)\n@RootNavGraph(start = true)\n@Destination\n@Composable\nfun HomeScreen(navigator: DestinationsNavigator) {\n    // Install from intent\n    var isIntentLaunched by rememberSaveable { mutableStateOf(false) }\n    val activity = LocalContext.current as Activity\n    val intent = activity.intent\n    LaunchedEffect(Unit) {\n        if (!isIntentLaunched && intent.action == Intent.ACTION_VIEW && intent.hasCategory(Intent.CATEGORY_DEFAULT) && intent.type == \"application/vnd.android.package-archive\") {\n            isIntentLaunched = true\n            val uri = intent.data\n            if (uri != null) {\n                navigator.navigate(ManageScreenDestination)\n                navigator.navigate(\n                    NewPatchScreenDestination(\n                        id = ACTION_INTENT_INSTALL,\n                        data = uri\n                    )\n                )\n            }\n        }\n    }\n\n    Scaffold(\n        topBar = { CenterTopBar(stringResource(R.string.app_name)) }\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .padding(horizontal = 16.dp)\n                .verticalScroll(rememberScrollState()),\n            verticalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            ShizukuCard()\n            InfoCard()\n            SupportCard()\n            Spacer(Modifier)\n        }\n    }\n}\n\nprivate val listener: (Int, Int) -> Unit = { _, grantResult ->\n    ShizukuApi.isPermissionGranted = grantResult == PackageManager.PERMISSION_GRANTED\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ShizukuCard() {\n    LaunchedEffect(Unit) {\n        Shizuku.addRequestPermissionResultListener(listener)\n    }\n    DisposableEffect(Unit) {\n        onDispose {\n            Shizuku.removeRequestPermissionResultListener(listener)\n        }\n    }\n\n    ElevatedCard(\n        colors = CardDefaults.elevatedCardColors(containerColor = run {\n            if (ShizukuApi.isPermissionGranted) MaterialTheme.colorScheme.secondaryContainer\n            else MaterialTheme.colorScheme.errorContainer\n        })\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable {\n                    if (ShizukuApi.isBinderAvailable && !ShizukuApi.isPermissionGranted) {\n                        Shizuku.requestPermission(114514)\n                    }\n                }\n                .padding(24.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            if (ShizukuApi.isPermissionGranted) {\n                Icon(Icons.Outlined.CheckCircle, stringResource(R.string.shizuku_available))\n                Column(Modifier.padding(start = 20.dp)) {\n                    Text(\n                        text = stringResource(R.string.shizuku_available),\n                        fontFamily = FontFamily.Serif,\n                        style = MaterialTheme.typography.titleMedium\n                    )\n                    Spacer(Modifier.height(4.dp))\n                    Text(\n                        text = \"API \" + Shizuku.getVersion(),\n                        style = MaterialTheme.typography.bodyMedium\n                    )\n                }\n            } else {\n                Icon(Icons.Outlined.Warning, stringResource(R.string.shizuku_unavailable))\n                Column(Modifier.padding(start = 20.dp)) {\n                    Text(\n                        text = stringResource(R.string.shizuku_unavailable),\n                        fontFamily = FontFamily.Serif,\n                        style = MaterialTheme.typography.titleMedium\n                    )\n                    Spacer(Modifier.height(4.dp))\n                    Text(\n                        text = stringResource(R.string.home_shizuku_warning),\n                        style = MaterialTheme.typography.bodyMedium\n                    )\n                }\n            }\n        }\n    }\n}\n\nprivate val apiVersion = if (Build.VERSION.PREVIEW_SDK_INT != 0) {\n    \"${Build.VERSION.CODENAME} Preview (API ${Build.VERSION.PREVIEW_SDK_INT})\"\n} else {\n    \"${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})\"\n}\n\nprivate val device = buildString {\n    append(Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1))\n    if (Build.BRAND != Build.MANUFACTURER) {\n        append(\" \" + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1))\n    }\n    append(\" \" + Build.MODEL + \" \")\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun InfoCard() {\n    val context = LocalContext.current\n    val snackbarHost = LocalSnackbarHost.current\n    val scope = rememberCoroutineScope()\n    ElevatedCard {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)\n        ) {\n            val contents = StringBuilder()\n            val infoCardContent: @Composable (Pair<String, String>) -> Unit = { texts ->\n                contents.appendLine(texts.first).appendLine(texts.second).appendLine()\n                Text(text = texts.first, style = MaterialTheme.typography.bodyLarge)\n                Text(text = texts.second, style = MaterialTheme.typography.bodyMedium)\n            }\n\n            infoCardContent(stringResource(R.string.home_api_version) to \"${LSPConfig.instance.API_CODE}\")\n\n            Spacer(Modifier.height(24.dp))\n            infoCardContent(stringResource(R.string.home_lspatch_version) to LSPConfig.instance.VERSION_NAME + \" (${LSPConfig.instance.VERSION_CODE})\")\n\n            Spacer(Modifier.height(24.dp))\n            infoCardContent(stringResource(R.string.home_framework_version) to LSPConfig.instance.CORE_VERSION_NAME + \" (${LSPConfig.instance.CORE_VERSION_CODE})\")\n\n            Spacer(Modifier.height(24.dp))\n            infoCardContent(stringResource(R.string.home_system_version) to apiVersion)\n\n            Spacer(Modifier.height(24.dp))\n            infoCardContent(stringResource(R.string.home_device) to device)\n\n            Spacer(Modifier.height(24.dp))\n            infoCardContent(stringResource(R.string.home_system_abi) to Build.SUPPORTED_ABIS[0])\n\n            val copiedMessage = stringResource(R.string.home_info_copied)\n            TextButton(\n                modifier = Modifier.align(Alignment.End),\n                onClick = {\n                    val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                    cm.setPrimaryClip(ClipData.newPlainText(\"LSPatch\", contents.toString()))\n                    scope.launch { snackbarHost.showSnackbar(copiedMessage) }\n                },\n                content = { Text(stringResource(android.R.string.copy)) }\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Preview\n@Composable\nprivate fun SupportCard() {\n    ElevatedCard {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(24.dp)\n        ) {\n            Text(\n                text = stringResource(R.string.home_support),\n                fontWeight = FontWeight.SemiBold,\n                style = MaterialTheme.typography.titleMedium\n            )\n            Text(\n                modifier = Modifier.padding(vertical = 8.dp),\n                text = stringResource(R.string.home_description),\n                style = MaterialTheme.typography.bodyMedium\n            )\n            HtmlText(\n                stringResource(\n                    R.string.home_view_source_code,\n                    \"<b><a href=\\\"https://github.com/LSPosed/LSPatch\\\">GitHub</a></b>\",\n                    \"<b><a href=\\\"https://t.me/LSPosed\\\">Telegram</a></b>\"\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/LogsScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport com.ramcosta.composedestinations.annotation.Destination\nimport org.lsposed.lspatch.ui.component.CenterTopBar\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Destination\n@Composable\nfun LogsScreen() {\n    Scaffold(\n        topBar = { CenterTopBar(stringResource(BottomBarDestination.Logs.label)) }\n    ) { innerPadding ->\n        Text(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize(),\n            text = \"This page is not yet implemented\",\n            textAlign = TextAlign.Center\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/ManageScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.google.accompanist.pager.ExperimentalPagerApi\nimport com.google.accompanist.pager.HorizontalPager\nimport com.google.accompanist.pager.rememberPagerState\nimport com.ramcosta.composedestinations.annotation.Destination\nimport com.ramcosta.composedestinations.navigation.DestinationsNavigator\nimport com.ramcosta.composedestinations.result.ResultRecipient\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.ui.component.CenterTopBar\nimport org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination\nimport org.lsposed.lspatch.ui.page.manage.AppManageBody\nimport org.lsposed.lspatch.ui.page.manage.AppManageFab\nimport org.lsposed.lspatch.ui.page.manage.ModuleManageBody\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)\n@Destination\n@Composable\nfun ManageScreen(\n    navigator: DestinationsNavigator,\n    resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>\n) {\n    val scope = rememberCoroutineScope()\n    val pagerState = rememberPagerState()\n    Scaffold(\n        topBar = { CenterTopBar(stringResource(BottomBarDestination.Manage.label)) },\n        floatingActionButton = { if (pagerState.currentPage == 0) AppManageFab(navigator) }\n    ) { innerPadding ->\n        Box(Modifier.padding(innerPadding)) {\n            Column {\n                TabRow(\n                    contentColor = MaterialTheme.colorScheme.secondary,\n                    selectedTabIndex = pagerState.currentPage\n                ) {\n                    Tab(\n                        selected = pagerState.currentPage == 0,\n                        onClick = { scope.launch { pagerState.animateScrollToPage(0) } }\n                    ) {\n                        Text(\n                            modifier = Modifier.padding(vertical = 16.dp),\n                            text = stringResource(R.string.apps)\n                        )\n                    }\n                    Tab(\n                        selected = pagerState.currentPage == 1,\n                        onClick = { scope.launch { pagerState.animateScrollToPage(1) } }\n                    ) {\n                        Text(\n                            modifier = Modifier.padding(vertical = 16.dp),\n                            text = stringResource(R.string.modules)\n                        )\n                    }\n                }\n\n                HorizontalPager(count = 2, state = pagerState) { page ->\n                    when (page) {\n                        0 -> AppManageBody(navigator, resultRecipient)\n                        1 -> ModuleManageBody()\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/NewPatchScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.pm.PackageInstaller\nimport android.net.Uri\nimport android.util.Log\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.*\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.ramcosta.composedestinations.annotation.Destination\nimport com.ramcosta.composedestinations.navigation.DestinationsNavigator\nimport com.ramcosta.composedestinations.result.NavResult\nimport com.ramcosta.composedestinations.result.ResultRecipient\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.ui.component.AnywhereDropdown\nimport org.lsposed.lspatch.ui.component.SelectionColumn\nimport org.lsposed.lspatch.ui.component.ShimmerAnimation\nimport org.lsposed.lspatch.ui.component.settings.SettingsCheckBox\nimport org.lsposed.lspatch.ui.component.settings.SettingsItem\nimport org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination\nimport org.lsposed.lspatch.ui.util.LocalSnackbarHost\nimport org.lsposed.lspatch.ui.util.isScrolledToEnd\nimport org.lsposed.lspatch.ui.util.lastItemIndex\nimport org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel\nimport org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.PatchState\nimport org.lsposed.lspatch.ui.viewmodel.NewPatchViewModel.ViewAction\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.LSPPackageManager.AppInfo\nimport org.lsposed.lspatch.util.ShizukuApi\n\nprivate const val TAG = \"NewPatchPage\"\n\nconst val ACTION_STORAGE = 0\nconst val ACTION_APPLIST = 1\nconst val ACTION_INTENT_INSTALL = 2\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Destination\n@Composable\nfun NewPatchScreen(\n    navigator: DestinationsNavigator,\n    resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>,\n    id: Int,\n    data: Uri? = null\n) {\n    val viewModel = viewModel<NewPatchViewModel>()\n    val snackbarHost = LocalSnackbarHost.current\n    val errorUnknown = stringResource(R.string.error_unknown)\n    val storageLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks ->\n        if (apks.isEmpty()) {\n            navigator.navigateUp()\n            return@rememberLauncherForActivityResult\n        }\n        runBlocking {\n            LSPPackageManager.getAppInfoFromApks(apks)\n                .onSuccess {\n                    viewModel.dispatch(ViewAction.ConfigurePatch(it.first()))\n                }\n                .onFailure {\n                    lspApp.globalScope.launch { snackbarHost.showSnackbar(it.message ?: errorUnknown) }\n                    navigator.navigateUp()\n                }\n        }\n    }\n\n    var showSelectModuleDialog by remember { mutableStateOf(false) }\n    val noXposedModules = stringResource(R.string.patch_no_xposed_module)\n    val storageModuleLauncher =\n        rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { apks ->\n            if (apks.isEmpty()) {\n                return@rememberLauncherForActivityResult\n            }\n            runBlocking {\n                LSPPackageManager.getAppInfoFromApks(apks).onSuccess { it ->\n                    viewModel.embeddedModules = it.filter { it.isXposedModule }.ifEmpty {\n                        lspApp.globalScope.launch {\n                            snackbarHost.showSnackbar(noXposedModules)\n                        }\n                        return@onSuccess\n                    }\n                }.onFailure {\n                    lspApp.globalScope.launch {\n                        snackbarHost.showSnackbar(\n                            it.message ?: errorUnknown\n                        )\n                    }\n                }\n            }\n        }\n\n    Log.d(TAG, \"PatchState: ${viewModel.patchState}\")\n    when (viewModel.patchState) {\n        PatchState.INIT -> {\n            LaunchedEffect(Unit) {\n                LSPPackageManager.cleanTmpApkDir()\n                when (id) {\n                    ACTION_STORAGE -> {\n                        storageLauncher.launch(arrayOf(\"application/vnd.android.package-archive\"))\n                        viewModel.dispatch(ViewAction.DoneInit)\n                    }\n\n                    ACTION_APPLIST -> {\n                        navigator.navigate(SelectAppsScreenDestination(false))\n                        viewModel.dispatch(ViewAction.DoneInit)\n                    }\n\n                    ACTION_INTENT_INSTALL -> {\n                        runBlocking {\n                            data?.let { uri ->\n                                LSPPackageManager.getAppInfoFromApks(listOf(uri)).onSuccess {\n                                    viewModel.dispatch(ViewAction.ConfigurePatch(it.first()))\n                                }.onFailure {\n                                    lspApp.globalScope.launch {\n                                        snackbarHost.showSnackbar(\n                                            it.message ?: errorUnknown\n                                        )\n                                    }\n                                    navigator.navigateUp()\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        PatchState.SELECTING -> {\n            resultRecipient.onNavResult {\n                Log.d(TAG, \"onNavResult: $it\")\n                when (it) {\n                    is NavResult.Canceled -> navigator.navigateUp()\n                    is NavResult.Value -> {\n                        val result = it.value as SelectAppsResult.SingleApp\n                        viewModel.dispatch(ViewAction.ConfigurePatch(result.selected))\n                    }\n                }\n            }\n        }\n        else -> {\n            Scaffold(\n                topBar = {\n                    when (viewModel.patchState) {\n                        PatchState.CONFIGURING -> ConfiguringTopBar { navigator.navigateUp() }\n                        PatchState.PATCHING,\n                        PatchState.FINISHED,\n                        PatchState.ERROR -> CenterAlignedTopAppBar(title = { Text(viewModel.patchApp.app.packageName) })\n                        else -> Unit\n                    }\n                },\n                floatingActionButton = {\n                    if (viewModel.patchState == PatchState.CONFIGURING) {\n                        ConfiguringFab()\n                    }\n                }\n            ) { innerPadding ->\n                if (viewModel.patchState == PatchState.CONFIGURING) {\n                    PatchOptionsBody(Modifier.padding(innerPadding)) {\n                        showSelectModuleDialog = true\n                    }\n                    resultRecipient.onNavResult {\n                        if (it is NavResult.Value) {\n                            val result = it.value as SelectAppsResult.MultipleApps\n                            viewModel.embeddedModules = result.selected\n                        }\n                    }\n                } else {\n                    DoPatchBody(Modifier.padding(innerPadding), navigator)\n                }\n            }\n\n            if (showSelectModuleDialog) {\n                AlertDialog(onDismissRequest = { showSelectModuleDialog = false },\n                    confirmButton = {},\n                    dismissButton = {\n                        TextButton(content = { Text(stringResource(android.R.string.cancel)) },\n                            onClick = { showSelectModuleDialog = false })\n                    },\n                    title = {\n                        Text(\n                            modifier = Modifier.fillMaxWidth(),\n                            text = stringResource(R.string.patch_embed_modules),\n                            textAlign = TextAlign.Center\n                        )\n                    },\n                    text = {\n                        Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {\n                            TextButton(modifier = Modifier.fillMaxWidth(),\n                                colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),\n                                onClick = {\n                                    storageModuleLauncher.launch(arrayOf(\"application/vnd.android.package-archive\"))\n                                    showSelectModuleDialog = false\n                                }) {\n                                Text(\n                                    modifier = Modifier.padding(vertical = 8.dp),\n                                    text = stringResource(R.string.patch_from_storage),\n                                    style = MaterialTheme.typography.bodyLarge\n                                )\n                            }\n                            TextButton(modifier = Modifier.fillMaxWidth(),\n                                colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),\n                                onClick = {\n                                    navigator.navigate(\n                                        SelectAppsScreenDestination(true,\n                                            viewModel.embeddedModules.mapTo(ArrayList()) { it.app.packageName })\n                                    )\n                                    showSelectModuleDialog = false\n                                }) {\n                                Text(\n                                    modifier = Modifier.padding(vertical = 8.dp),\n                                    text = stringResource(R.string.patch_from_applist),\n                                    style = MaterialTheme.typography.bodyLarge\n                                )\n                            }\n                        }\n                    })\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ConfiguringTopBar(onBackClick: () -> Unit) {\n    TopAppBar(\n        title = { Text(stringResource(R.string.screen_new_patch)) },\n        navigationIcon = {\n            IconButton(\n                onClick = onBackClick,\n                content = { Icon(Icons.Outlined.ArrowBack, null) }\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun ConfiguringFab() {\n    val viewModel = viewModel<NewPatchViewModel>()\n    ExtendedFloatingActionButton(\n        text = { Text(stringResource(R.string.patch_start)) },\n        icon = { Icon(Icons.Outlined.AutoFixHigh, null) },\n        onClick = { viewModel.dispatch(ViewAction.SubmitPatch) }\n    )\n}\n\n@Composable\nprivate fun sigBypassLvStr(level: Int) = when (level) {\n    0 -> stringResource(R.string.patch_sigbypasslv0)\n    1 -> stringResource(R.string.patch_sigbypasslv1)\n    2 -> stringResource(R.string.patch_sigbypasslv2)\n    else -> throw IllegalArgumentException(\"Invalid sigBypassLv: $level\")\n}\n\n@Composable\nprivate fun PatchOptionsBody(modifier: Modifier, onAddEmbed: () -> Unit) {\n    val viewModel = viewModel<NewPatchViewModel>()\n\n    Column(modifier.verticalScroll(rememberScrollState())) {\n        Text(\n            text = viewModel.patchApp.label,\n            style = MaterialTheme.typography.headlineSmall,\n            modifier = Modifier.padding(horizontal = 24.dp)\n        )\n        Text(\n            text = viewModel.patchApp.app.packageName,\n            style = MaterialTheme.typography.bodyLarge,\n            modifier = Modifier.padding(horizontal = 24.dp)\n        )\n        Text(\n            text = stringResource(R.string.patch_mode),\n            style = MaterialTheme.typography.titleLarge,\n            modifier = Modifier\n                .align(Alignment.CenterHorizontally)\n                .padding(top = 24.dp, bottom = 12.dp)\n        )\n        SelectionColumn(Modifier.padding(horizontal = 24.dp)) {\n            SelectionItem(\n                selected = viewModel.useManager,\n                onClick = { viewModel.useManager = true },\n                icon = Icons.Outlined.Api,\n                title = stringResource(R.string.patch_local),\n                desc = stringResource(R.string.patch_local_desc)\n            )\n            SelectionItem(\n                selected = !viewModel.useManager,\n                onClick = { viewModel.useManager = false },\n                icon = Icons.Outlined.WorkOutline,\n                title = stringResource(R.string.patch_integrated),\n                desc = stringResource(R.string.patch_integrated_desc),\n                extraContent = {\n                    TextButton(\n                        onClick = onAddEmbed,\n                        content = { Text(text = stringResource(R.string.patch_embed_modules), style = MaterialTheme.typography.bodyLarge) }\n                    )\n                }\n            )\n        }\n        SettingsCheckBox(\n            modifier = Modifier\n                .padding(top = 6.dp)\n                .clickable { viewModel.debuggable = !viewModel.debuggable },\n            checked = viewModel.debuggable,\n            icon = Icons.Outlined.BugReport,\n            title = stringResource(R.string.patch_debuggable)\n        )\n        SettingsCheckBox(\n            modifier = Modifier.clickable { viewModel.overrideVersionCode = !viewModel.overrideVersionCode },\n            checked = viewModel.overrideVersionCode,\n            icon = Icons.Outlined.Layers,\n            title = stringResource(R.string.patch_override_version_code),\n            desc = stringResource(R.string.patch_override_version_code_desc)\n        )\n        var bypassExpanded by remember { mutableStateOf(false) }\n        AnywhereDropdown(\n            expanded = bypassExpanded,\n            onDismissRequest = { bypassExpanded = false },\n            onClick = { bypassExpanded = true },\n            surface = {\n                SettingsItem(\n                    icon = Icons.Outlined.RemoveModerator,\n                    title = stringResource(R.string.patch_sigbypass),\n                    desc = sigBypassLvStr(viewModel.sigBypassLevel)\n                )\n            }\n        ) {\n            repeat(3) {\n                DropdownMenuItem(\n                    text = {\n                        Row(verticalAlignment = Alignment.CenterVertically) {\n                            RadioButton(selected = viewModel.sigBypassLevel == it, onClick = { viewModel.sigBypassLevel = it })\n                            Text(sigBypassLvStr(it))\n                        }\n                    },\n                    onClick = {\n                        viewModel.sigBypassLevel = it\n                        bypassExpanded = false\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun DoPatchBody(modifier: Modifier, navigator: DestinationsNavigator) {\n    val viewModel = viewModel<NewPatchViewModel>()\n    val snackbarHost = LocalSnackbarHost.current\n    val scope = rememberCoroutineScope()\n\n    LaunchedEffect(Unit) {\n        if (viewModel.logs.isEmpty()) {\n            viewModel.dispatch(ViewAction.LaunchPatch)\n        }\n    }\n\n    BoxWithConstraints(modifier.padding(start = 24.dp, end = 24.dp, bottom = 24.dp)) {\n        val shellBoxMaxHeight =\n            if (viewModel.patchState == PatchState.PATCHING) maxHeight\n            else maxHeight - ButtonDefaults.MinHeight - 12.dp\n        Column(\n            Modifier\n                .fillMaxSize()\n                .wrapContentHeight()\n                .animateContentSize(spring(stiffness = Spring.StiffnessLow))\n        ) {\n            ShimmerAnimation(enabled = viewModel.patchState == PatchState.PATCHING) {\n                ProvideTextStyle(MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)) {\n                    val scrollState = rememberLazyListState()\n                    LazyColumn(\n                        state = scrollState,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .heightIn(max = shellBoxMaxHeight)\n                            .clip(RoundedCornerShape(32.dp))\n                            .background(brush)\n                            .padding(horizontal = 24.dp, vertical = 18.dp)\n                    ) {\n                        items(viewModel.logs) {\n                            when (it.first) {\n                                Log.DEBUG -> Text(text = it.second)\n                                Log.INFO -> Text(text = it.second)\n                                Log.ERROR -> Text(text = it.second, color = MaterialTheme.colorScheme.error)\n                            }\n                        }\n                    }\n\n                    LaunchedEffect(scrollState.lastItemIndex) {\n                        if (!scrollState.isScrolledToEnd) {\n                            scrollState.animateScrollToItem(scrollState.lastItemIndex!!)\n                        }\n                    }\n                }\n            }\n\n            when (viewModel.patchState) {\n                PatchState.PATCHING -> BackHandler {}\n                PatchState.FINISHED -> {\n                    val shizukuUnavailable = stringResource(R.string.shizuku_unavailable)\n                    val installSuccessfully = stringResource(R.string.patch_install_successfully)\n                    val installFailed = stringResource(R.string.patch_install_failed)\n                    val copyError = stringResource(R.string.copy_error)\n                    var installing by remember { mutableStateOf(false) }\n                    if (installing) InstallDialog(viewModel.patchApp) { status, message ->\n                        scope.launch {\n                            installing = false\n                            if (status == PackageInstaller.STATUS_SUCCESS) {\n                                lspApp.globalScope.launch { snackbarHost.showSnackbar(installSuccessfully) }\n                                navigator.navigateUp()\n                            } else if (status != LSPPackageManager.STATUS_USER_CANCELLED) {\n                                val result = snackbarHost.showSnackbar(installFailed, copyError)\n                                if (result == SnackbarResult.ActionPerformed) {\n                                    val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                                    cm.setPrimaryClip(ClipData.newPlainText(\"LSPatch\", message))\n                                }\n                            }\n                        }\n                    }\n                    Row(Modifier.padding(top = 12.dp)) {\n                        Button(\n                            modifier = Modifier.weight(1f),\n                            onClick = { navigator.navigateUp() },\n                            content = { Text(stringResource(R.string.patch_return)) }\n                        )\n                        Spacer(Modifier.weight(0.2f))\n                        Button(\n                            modifier = Modifier.weight(1f),\n                            onClick = {\n                                if (!ShizukuApi.isPermissionGranted) {\n                                    scope.launch {\n                                        snackbarHost.showSnackbar(shizukuUnavailable)\n                                    }\n                                } else {\n                                    installing = true\n                                }\n                            },\n                            content = { Text(stringResource(R.string.install)) }\n                        )\n                    }\n                }\n                PatchState.ERROR -> {\n                    Row(Modifier.padding(top = 12.dp)) {\n                        Button(\n                            modifier = Modifier.weight(1f),\n                            onClick = { navigator.navigateUp() },\n                            content = { Text(stringResource(R.string.patch_return)) }\n                        )\n                        Spacer(Modifier.weight(0.2f))\n                        Button(\n                            modifier = Modifier.weight(1f),\n                            onClick = {\n                                val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                                cm.setPrimaryClip(ClipData.newPlainText(\"LSPatch\", viewModel.logs.joinToString { it.second + \"\\n\" }))\n                            },\n                            content = { Text(stringResource(R.string.copy_error)) }\n                        )\n                    }\n                }\n                else -> Unit\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InstallDialog(patchApp: AppInfo, onFinish: (Int, String?) -> Unit) {\n    val scope = rememberCoroutineScope()\n    var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }\n    var installing by remember { mutableStateOf(0) }\n    suspend fun doInstall() {\n        Log.i(TAG, \"Installing app ${patchApp.app.packageName}\")\n        installing = 1\n        val (status, message) = LSPPackageManager.install()\n        installing = 0\n        Log.i(TAG, \"Installation end: $status, $message\")\n        onFinish(status, message)\n    }\n\n    LaunchedEffect(Unit) {\n        if (!uninstallFirst) {\n            doInstall()\n        }\n    }\n\n    if (uninstallFirst) {\n        AlertDialog(\n            onDismissRequest = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, \"User cancelled\") },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        scope.launch {\n                            Log.i(TAG, \"Uninstalling app ${patchApp.app.packageName}\")\n                            uninstallFirst = false\n                            installing = 2\n                            val (status, message) = LSPPackageManager.uninstall(patchApp.app.packageName)\n                            installing = 0\n                            Log.i(TAG, \"Uninstallation end: $status, $message\")\n                            if (status == PackageInstaller.STATUS_SUCCESS) {\n                                doInstall()\n                            } else {\n                                onFinish(status, message)\n                            }\n                        }\n                    },\n                    content = { Text(stringResource(android.R.string.ok)) }\n                )\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = { onFinish(LSPPackageManager.STATUS_USER_CANCELLED, \"User cancelled\") },\n                    content = { Text(stringResource(android.R.string.cancel)) }\n                )\n            },\n            title = {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = stringResource(R.string.uninstall),\n                    textAlign = TextAlign.Center\n                )\n            },\n            text = { Text(stringResource(R.string.patch_uninstall_text)) }\n        )\n    }\n\n    if (installing != 0) {\n        AlertDialog(\n            onDismissRequest = {},\n            confirmButton = {},\n            title = {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = stringResource(if (installing == 1) R.string.installing else R.string.uninstalling),\n                    fontFamily = FontFamily.Serif,\n                    textAlign = TextAlign.Center\n                )\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/RepoScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport com.ramcosta.composedestinations.annotation.Destination\nimport org.lsposed.lspatch.ui.component.CenterTopBar\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Destination\n@Composable\nfun RepoScreen() {\n    Scaffold(\n        topBar = { CenterTopBar(stringResource(BottomBarDestination.Repo.label)) }\n    ) { innerPadding ->\n        Text(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize(),\n            text = \"This page is not yet implemented\",\n            textAlign = TextAlign.Center\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/SelectAppsScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport android.content.pm.ApplicationInfo\nimport android.os.Parcelable\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Done\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.intl.Locale\nimport androidx.compose.ui.text.toLowerCase\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.google.accompanist.swiperefresh.SwipeRefresh\nimport com.google.accompanist.swiperefresh.rememberSwipeRefreshState\nimport com.ramcosta.composedestinations.annotation.Destination\nimport com.ramcosta.composedestinations.result.ResultBackNavigator\nimport kotlinx.parcelize.Parcelize\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.ui.component.AppItem\nimport org.lsposed.lspatch.ui.component.SearchAppBar\nimport org.lsposed.lspatch.ui.viewmodel.SelectAppsViewModel\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.LSPPackageManager.AppInfo\n\n@Parcelize\nsealed class SelectAppsResult : Parcelable {\n    data class SingleApp(val selected: AppInfo) : SelectAppsResult()\n    data class MultipleApps(val selected: List<AppInfo>) : SelectAppsResult()\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Destination\n@Composable\nfun SelectAppsScreen(\n    navigator: ResultBackNavigator<SelectAppsResult>,\n    multiSelect: Boolean,\n    initialSelected: ArrayList<String>? = null\n) {\n    val viewModel = viewModel<SelectAppsViewModel>()\n\n    var searchPackage by remember { mutableStateOf(\"\") }\n    val filter: (AppInfo) -> Boolean = {\n        val packageLowerCase = searchPackage.toLowerCase(Locale.current)\n        val contains = it.label.toLowerCase(Locale.current).contains(packageLowerCase) || it.app.packageName.contains(packageLowerCase)\n        if (multiSelect) contains && it.isXposedModule\n        else contains && it.app.flags and ApplicationInfo.FLAG_SYSTEM == 0\n    }\n\n    LaunchedEffect(Unit) {\n        viewModel.filterAppList(false, filter)\n        initialSelected?.let {\n            val tmp = initialSelected.toSet()\n            viewModel.multiSelected.addAll(LSPPackageManager.appList.filter { tmp.contains(it.app.packageName) })\n        }\n    }\n\n    BackHandler {\n        navigator.navigateBack()\n    }\n\n    Scaffold(\n        topBar = {\n            SearchAppBar(\n                title = { Text(stringResource(R.string.screen_select_apps)) },\n                searchText = searchPackage,\n                onSearchTextChange = {\n                    searchPackage = it\n                    viewModel.filterAppList(false, filter)\n                },\n                onClearClick = {\n                    searchPackage = \"\"\n                    viewModel.filterAppList(false, filter)\n                },\n                onBackClick = {\n                    navigator.navigateBack()\n                }\n            )\n        },\n        floatingActionButton = {\n            if (multiSelect) MultiSelectFab {\n                navigator.navigateBack(SelectAppsResult.MultipleApps(viewModel.multiSelected))\n            }\n        }\n    ) { innerPadding ->\n        SwipeRefresh(\n            state = rememberSwipeRefreshState(viewModel.isRefreshing),\n            onRefresh = { viewModel.filterAppList(true, filter) },\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n        ) {\n            if (multiSelect) MultiSelect()\n            else SingleSelect {\n                navigator.navigateBack(SelectAppsResult.SingleApp(it))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MultiSelectFab(onClick: () -> Unit) {\n    FloatingActionButton(\n        onClick = onClick,\n        content = { Icon(Icons.Outlined.Done, stringResource(R.string.add)) }\n    )\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun SingleSelect(onSelect: (AppInfo) -> Unit) {\n    val viewModel = viewModel<SelectAppsViewModel>()\n    LazyColumn {\n        items(\n            items = viewModel.filteredList,\n            key = { it.app.packageName }\n        ) {\n            AppItem(\n                modifier = Modifier\n                    .animateItemPlacement(spring(stiffness = Spring.StiffnessLow))\n                    .clickable { onSelect(it) },\n                icon = LSPPackageManager.getIcon(it),\n                label = it.label,\n                packageName = it.app.packageName\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun MultiSelect() {\n    val viewModel = viewModel<SelectAppsViewModel>()\n    LazyColumn {\n        items(\n            items = viewModel.filteredList,\n            key = { it.app.packageName }\n        ) {\n            val checked = viewModel.multiSelected.contains(it)\n            AppItem(\n                modifier = Modifier\n                    .animateItemPlacement(spring(stiffness = Spring.StiffnessLow))\n                    .clickable {\n                        if (checked) viewModel.multiSelected.remove(it)\n                        else viewModel.multiSelected.add(it)\n                    },\n                icon = LSPPackageManager.getIcon(it),\n                label = it.label,\n                packageName = it.app.packageName,\n                checked = checked\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsScreen.kt",
    "content": "package org.lsposed.lspatch.ui.page\n\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.PressInteraction\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Ballot\nimport androidx.compose.material.icons.outlined.BugReport\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.ramcosta.composedestinations.annotation.Destination\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.config.Configs\nimport org.lsposed.lspatch.config.MyKeyStore\nimport org.lsposed.lspatch.ui.component.AnywhereDropdown\nimport org.lsposed.lspatch.ui.component.CenterTopBar\nimport org.lsposed.lspatch.ui.component.settings.SettingsItem\nimport org.lsposed.lspatch.ui.component.settings.SettingsSwitch\nimport java.io.IOException\nimport java.security.GeneralSecurityException\nimport java.security.KeyStore\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Destination\n@Composable\nfun SettingsScreen() {\n    Scaffold(\n        topBar = { CenterTopBar(stringResource(BottomBarDestination.Settings.label)) }\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .verticalScroll(rememberScrollState())\n        ) {\n            KeyStore()\n            DetailPatchLogs()\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun KeyStore() {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    var expanded by remember { mutableStateOf(false) }\n    var showDialog by remember { mutableStateOf(false) }\n\n    AnywhereDropdown(\n        expanded = expanded,\n        onDismissRequest = { expanded = false },\n        onClick = { expanded = true },\n        surface = {\n            SettingsItem(\n                icon = Icons.Outlined.Ballot,\n                title = stringResource(R.string.settings_keystore),\n                desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom)\n            )\n        }\n    ) {\n        DropdownMenuItem(\n            text = { Text(stringResource(R.string.settings_keystore_default)) },\n            onClick = {\n                scope.launch { MyKeyStore.reset() }\n                expanded = false\n            }\n        )\n        DropdownMenuItem(\n            text = { Text(stringResource(R.string.settings_keystore_custom)) },\n            onClick = {\n                expanded = false\n                showDialog = true\n            }\n        )\n    }\n\n    if (showDialog) {\n        var wrongKeystore by rememberSaveable { mutableStateOf(false) }\n        var wrongPassword by rememberSaveable { mutableStateOf(false) }\n        var wrongAliasName by rememberSaveable { mutableStateOf(false) }\n        var wrongAliasPassword by rememberSaveable { mutableStateOf(false) }\n\n        var path by rememberSaveable { mutableStateOf(\"\") }\n        var password by rememberSaveable { mutableStateOf(\"\") }\n        var alias by rememberSaveable { mutableStateOf(\"\") }\n        var aliasPassword by rememberSaveable { mutableStateOf(\"\") }\n\n        val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->\n            if (uri == null) return@rememberLauncherForActivityResult\n            context.contentResolver.openInputStream(uri).use { input ->\n                MyKeyStore.tmpFile.outputStream().use { output ->\n                    input?.copyTo(output)\n                }\n            }\n            path = uri.path ?: \"\"\n        }\n\n        AlertDialog(\n            onDismissRequest = { expanded = false; showDialog = false },\n            confirmButton = {\n                TextButton(\n                    content = { Text(stringResource(android.R.string.ok)) },\n                    onClick = {\n                        wrongKeystore = false\n                        wrongPassword = false\n                        wrongAliasName = false\n                        wrongAliasPassword = false\n\n                        if (path.isEmpty()) {\n                            wrongKeystore = true\n                            return@TextButton\n                        }\n                        val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())\n                        try {\n                            MyKeyStore.tmpFile.inputStream().use { input ->\n                                keyStore.load(input, password.toCharArray())\n                            }\n                        } catch (e: IOException) {\n                            wrongKeystore = true\n                            if (e.message == \"KeyStore integrity check failed.\") {\n                                wrongPassword = true\n                            }\n                            return@TextButton\n                        }\n                        if (!keyStore.containsAlias(alias)) {\n                            wrongAliasName = true\n                            return@TextButton\n                        }\n                        try {\n                            keyStore.getKey(alias, aliasPassword.toCharArray())\n                        } catch (e: GeneralSecurityException) {\n                            wrongAliasPassword = true\n                            return@TextButton\n                        }\n\n                        scope.launch { MyKeyStore.setCustom(password, alias, aliasPassword) }\n                        expanded = false\n                        showDialog = false\n                    })\n            },\n            dismissButton = {\n                TextButton(\n                    content = { Text(stringResource(android.R.string.cancel)) },\n                    onClick = { expanded = false; showDialog = false }\n                )\n            },\n            title = {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = stringResource(R.string.settings_keystore_dialog_title),\n                    textAlign = TextAlign.Center\n                )\n            },\n            text = {\n                Column(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    val interactionSource = remember { MutableInteractionSource() }\n                    LaunchedEffect(interactionSource) {\n                        interactionSource.interactions.collect { interaction ->\n                            if (interaction is PressInteraction.Release) {\n                                launcher.launch(\"*/*\")\n                            }\n                        }\n                    }\n\n                    val wrongText = when {\n                        wrongAliasPassword -> stringResource(R.string.settings_keystore_wrong_alias_password)\n                        wrongAliasName -> stringResource(R.string.settings_keystore_wrong_alias)\n                        wrongPassword -> stringResource(R.string.settings_keystore_wrong_password)\n                        wrongKeystore -> stringResource(R.string.settings_keystore_wrong_keystore)\n                        else -> null\n                    }\n                    Text(\n                        modifier = Modifier.padding(bottom = 8.dp),\n                        text = wrongText ?: stringResource(R.string.settings_keystore_desc),\n                        color = if (wrongText != null) MaterialTheme.colorScheme.error else Color.Unspecified\n                    )\n\n                    OutlinedTextField(\n                        value = path,\n                        onValueChange = { path = it },\n                        readOnly = true,\n                        label = { Text(stringResource(R.string.settings_keystore_file)) },\n                        placeholder = { Text(stringResource(R.string.settings_keystore_file)) },\n                        singleLine = true,\n                        isError = wrongKeystore,\n                        interactionSource = interactionSource\n                    )\n                    OutlinedTextField(\n                        value = password,\n                        onValueChange = { password = it },\n                        label = { Text(stringResource(R.string.settings_keystore_password)) },\n                        singleLine = true,\n                        isError = wrongPassword\n                    )\n                    OutlinedTextField(\n                        value = alias,\n                        onValueChange = { alias = it },\n                        label = { Text(stringResource(R.string.settings_keystore_alias)) },\n                        singleLine = true,\n                        isError = wrongAliasName\n                    )\n                    OutlinedTextField(\n                        value = aliasPassword,\n                        onValueChange = { aliasPassword = it },\n                        label = { Text(stringResource(R.string.settings_keystore_alias_password)) },\n                        singleLine = true,\n                        isError = wrongAliasPassword\n                    )\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun DetailPatchLogs() {\n    SettingsSwitch(\n        modifier = Modifier.clickable { Configs.detailPatchLogs = !Configs.detailPatchLogs },\n        checked = Configs.detailPatchLogs,\n        icon = Icons.Outlined.BugReport,\n        title = stringResource(R.string.settings_detail_patch_logs)\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/manage/AppManagePage.kt",
    "content": "package org.lsposed.lspatch.ui.page.manage\n\nimport android.app.Activity\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.util.Log\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.KeyboardCapslock\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.documentfile.provider.DocumentFile\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.ramcosta.composedestinations.navigation.DestinationsNavigator\nimport com.ramcosta.composedestinations.result.NavResult\nimport com.ramcosta.composedestinations.result.ResultRecipient\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.BuildConfig\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.config.ConfigManager\nimport org.lsposed.lspatch.config.Configs\nimport org.lsposed.lspatch.database.entity.Module\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.share.Constants\nimport org.lsposed.lspatch.share.LSPConfig\nimport org.lsposed.lspatch.ui.component.AnywhereDropdown\nimport org.lsposed.lspatch.ui.component.AppItem\nimport org.lsposed.lspatch.ui.component.LoadingDialog\nimport org.lsposed.lspatch.ui.page.ACTION_APPLIST\nimport org.lsposed.lspatch.ui.page.ACTION_STORAGE\nimport org.lsposed.lspatch.ui.page.SelectAppsResult\nimport org.lsposed.lspatch.ui.page.destinations.NewPatchScreenDestination\nimport org.lsposed.lspatch.ui.page.destinations.SelectAppsScreenDestination\nimport org.lsposed.lspatch.ui.util.LocalSnackbarHost\nimport org.lsposed.lspatch.ui.viewmodel.manage.AppManageViewModel\nimport org.lsposed.lspatch.ui.viewstate.ProcessingState\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.ShizukuApi\nimport java.io.IOException\n\nprivate const val TAG = \"AppManagePage\"\n\n@Composable\nfun AppManageBody(\n    navigator: DestinationsNavigator,\n    resultRecipient: ResultRecipient<SelectAppsScreenDestination, SelectAppsResult>\n) {\n    val viewModel = viewModel<AppManageViewModel>()\n    val snackbarHost = LocalSnackbarHost.current\n    val scope = rememberCoroutineScope()\n\n    if (viewModel.appList.isEmpty()) {\n        Box(Modifier.fillMaxSize()) {\n            Text(\n                modifier = Modifier.align(Alignment.Center),\n                text = run {\n                    if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading)\n                    else stringResource(R.string.manage_no_apps)\n                },\n                fontFamily = FontFamily.Serif,\n                style = MaterialTheme.typography.headlineSmall\n            )\n        }\n    } else {\n        var scopeApp by rememberSaveable { mutableStateOf(\"\") }\n        resultRecipient.onNavResult {\n            if (it is NavResult.Value) {\n                scope.launch {\n                    val result = it.value as SelectAppsResult.MultipleApps\n                    ConfigManager.getModulesForApp(scopeApp).forEach {\n                        ConfigManager.deactivateModule(scopeApp, it)\n                    }\n                    result.selected.forEach {\n                        Log.d(TAG, \"Activate ${it.app.packageName} for $scopeApp\")\n                        ConfigManager.activateModule(scopeApp, Module(it.app.packageName, it.app.sourceDir))\n                    }\n                }\n            }\n        }\n\n        when (viewModel.updateLoaderState) {\n            is ProcessingState.Idle -> Unit\n            is ProcessingState.Processing -> LoadingDialog()\n            is ProcessingState.Done -> {\n                val it = viewModel.updateLoaderState as ProcessingState.Done\n                val updateSuccessfully = stringResource(R.string.manage_update_loader_successfully)\n                val updateFailed = stringResource(R.string.manage_update_loader_failed)\n                val copyError = stringResource(R.string.copy_error)\n                LaunchedEffect(Unit) {\n                    it.result.onSuccess {\n                        snackbarHost.showSnackbar(updateSuccessfully)\n                    }.onFailure {\n                        val result = snackbarHost.showSnackbar(updateFailed, copyError)\n                        if (result == SnackbarResult.ActionPerformed) {\n                            val cm = lspApp.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                            cm.setPrimaryClip(ClipData.newPlainText(\"LSPatch\", it.toString()))\n                        }\n                    }\n                    viewModel.dispatch(AppManageViewModel.ViewAction.ClearUpdateLoaderResult)\n                }\n            }\n        }\n        when (viewModel.optimizeState) {\n            is ProcessingState.Idle -> Unit\n            is ProcessingState.Processing -> LoadingDialog()\n            is ProcessingState.Done -> {\n                val it = viewModel.optimizeState as ProcessingState.Done\n                val optimizeSucceed = stringResource(R.string.manage_optimize_successfully)\n                val optimizeFailed = stringResource(R.string.manage_optimize_failed)\n                LaunchedEffect(Unit) {\n                    snackbarHost.showSnackbar(if (it.result) optimizeSucceed else optimizeFailed)\n                    viewModel.dispatch(AppManageViewModel.ViewAction.ClearOptimizeResult)\n                }\n            }\n        }\n\n        LazyColumn(Modifier.fillMaxHeight()) {\n            items(\n                items = viewModel.appList,\n                key = { it.first.app.packageName }\n            ) {\n                val isRolling = it.second.useManager && it.second.lspConfig.VERSION_CODE >= Constants.MIN_ROLLING_VERSION_CODE\n                val canUpdateLoader = !isRolling && it.second.lspConfig.VERSION_CODE < LSPConfig.instance.VERSION_CODE\n                var expanded by remember { mutableStateOf(false) }\n                AnywhereDropdown(\n                    expanded = expanded,\n                    onDismissRequest = { expanded = false },\n                    onClick = { expanded = true },\n                    onLongClick = { expanded = true },\n                    surface = {\n                        AppItem(\n                            icon = LSPPackageManager.getIcon(it.first),\n                            label = it.first.label,\n                            packageName = it.first.app.packageName,\n                            additionalContent = {\n                                Row(verticalAlignment = Alignment.CenterVertically) {\n                                    Text(\n                                        text = buildAnnotatedString {\n                                            val (text, color) =\n                                                if (it.second.useManager) stringResource(R.string.patch_local) to MaterialTheme.colorScheme.secondary\n                                                else stringResource(R.string.patch_integrated) to MaterialTheme.colorScheme.tertiary\n                                            append(AnnotatedString(text, SpanStyle(color = color)))\n                                            append(\"  \")\n                                            if (isRolling) append(stringResource(R.string.manage_rolling))\n                                            else append(it.second.lspConfig.VERSION_CODE.toString())\n                                        },\n                                        fontWeight = FontWeight.SemiBold,\n                                        fontFamily = FontFamily.Serif,\n                                        style = MaterialTheme.typography.bodySmall\n                                    )\n                                    if (canUpdateLoader) {\n                                        with(LocalDensity.current) {\n                                            val size = MaterialTheme.typography.bodySmall.fontSize * 1.2\n                                            Icon(Icons.Filled.KeyboardCapslock, null, Modifier.size(size.toDp()))\n                                        }\n                                    }\n                                }\n                            }\n                        )\n                    }\n                ) {\n                    DropdownMenuItem(\n                        text = { Text(text = it.first.label, color = MaterialTheme.colorScheme.primary) },\n                        onClick = {}, enabled = false\n                    )\n                    val shizukuUnavailable = stringResource(R.string.shizuku_unavailable)\n                    if (canUpdateLoader || BuildConfig.DEBUG) {\n                        DropdownMenuItem(\n                            text = { Text(stringResource(R.string.manage_update_loader)) },\n                            onClick = {\n                                expanded = false\n                                scope.launch {\n                                    if (!ShizukuApi.isPermissionGranted) {\n                                        snackbarHost.showSnackbar(shizukuUnavailable)\n                                    } else {\n                                        viewModel.dispatch(AppManageViewModel.ViewAction.UpdateLoader(it.first, it.second))\n                                    }\n                                }\n                            }\n                        )\n                    }\n                    if (it.second.useManager) {\n                        DropdownMenuItem(\n                            text = { Text(stringResource(R.string.manage_module_scope)) },\n                            onClick = {\n                                expanded = false\n                                scope.launch {\n                                    scopeApp = it.first.app.packageName\n                                    val activated = ConfigManager.getModulesForApp(scopeApp).map { it.pkgName }.toSet()\n                                    val initialSelected = LSPPackageManager.appList.mapNotNullTo(ArrayList()) {\n                                        if (activated.contains(it.app.packageName)) it.app.packageName else null\n                                    }\n                                    navigator.navigate(SelectAppsScreenDestination(true, initialSelected))\n                                }\n                            }\n                        )\n                    }\n                    DropdownMenuItem(\n                        text = { Text(stringResource(R.string.manage_optimize)) },\n                        onClick = {\n                            expanded = false\n                            scope.launch {\n                                if (!ShizukuApi.isPermissionGranted) {\n                                    snackbarHost.showSnackbar(shizukuUnavailable)\n                                } else {\n                                    viewModel.dispatch(AppManageViewModel.ViewAction.PerformOptimize(it.first))\n                                }\n                            }\n                        }\n                    )\n                    val uninstallSuccessfully = stringResource(R.string.manage_uninstall_successfully)\n                    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->\n                        if (result.resultCode == Activity.RESULT_OK) {\n                            scope.launch {\n                                snackbarHost.showSnackbar(uninstallSuccessfully)\n                            }\n                        }\n                    }\n                    DropdownMenuItem(\n                        text = { Text(stringResource(R.string.uninstall)) },\n                        onClick = {\n                            expanded = false\n                            val intent = Intent(Intent.ACTION_DELETE).apply {\n                                data = Uri.parse(\"package:${it.first.app.packageName}\")\n                                putExtra(Intent.EXTRA_RETURN_RESULT, true)\n                            }\n                            launcher.launch(intent)\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun AppManageFab(navigator: DestinationsNavigator) {\n    val context = LocalContext.current\n    val snackbarHost = LocalSnackbarHost.current\n    val scope = rememberCoroutineScope()\n    var shouldSelectDirectory by remember { mutableStateOf(false) }\n    var showNewPatchDialog by remember { mutableStateOf(false) }\n\n    val errorText = stringResource(R.string.patch_select_dir_error)\n    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {\n        try {\n            if (it.resultCode == Activity.RESULT_CANCELED) return@rememberLauncherForActivityResult\n            val uri = it.data?.data ?: throw IOException(\"No data\")\n            val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION\n            context.contentResolver.takePersistableUriPermission(uri, takeFlags)\n            Configs.storageDirectory = uri.toString()\n            Log.i(TAG, \"Storage directory: ${uri.path}\")\n            showNewPatchDialog = true\n        } catch (e: Exception) {\n            Log.e(TAG, \"Error when requesting saving directory\", e)\n            scope.launch { snackbarHost.showSnackbar(errorText) }\n        }\n    }\n\n    if (shouldSelectDirectory) {\n        AlertDialog(\n            onDismissRequest = { shouldSelectDirectory = false },\n            confirmButton = {\n                TextButton(\n                    content = { Text(stringResource(android.R.string.ok)) },\n                    onClick = {\n                        launcher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))\n                        shouldSelectDirectory = false\n                    }\n                )\n            },\n            dismissButton = {\n                TextButton(\n                    content = { Text(stringResource(android.R.string.cancel)) },\n                    onClick = { shouldSelectDirectory = false }\n                )\n            },\n            title = {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = stringResource(R.string.patch_select_dir_title),\n                    textAlign = TextAlign.Center\n                )\n            },\n            text = { Text(stringResource(R.string.patch_select_dir_text)) }\n        )\n    }\n\n    if (showNewPatchDialog) {\n        AlertDialog(\n            onDismissRequest = { showNewPatchDialog = false },\n            confirmButton = {},\n            dismissButton = {\n                TextButton(\n                    content = { Text(stringResource(android.R.string.cancel)) },\n                    onClick = { showNewPatchDialog = false }\n                )\n            },\n            title = {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = stringResource(R.string.screen_new_patch),\n                    textAlign = TextAlign.Center\n                )\n            },\n            text = {\n                Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {\n                    TextButton(\n                        modifier = Modifier.fillMaxWidth(),\n                        colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),\n                        onClick = {\n                            navigator.navigate(NewPatchScreenDestination(id = ACTION_STORAGE))\n                            showNewPatchDialog = false\n                        }\n                    ) {\n                        Text(\n                            modifier = Modifier.padding(vertical = 8.dp),\n                            text = stringResource(R.string.patch_from_storage),\n                            style = MaterialTheme.typography.bodyLarge\n                        )\n                    }\n                    TextButton(\n                        modifier = Modifier.fillMaxWidth(),\n                        colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.secondary),\n                        onClick = {\n                            navigator.navigate(NewPatchScreenDestination(id = ACTION_APPLIST))\n                            showNewPatchDialog = false\n                        }\n                    ) {\n                        Text(\n                            modifier = Modifier.padding(vertical = 8.dp),\n                            text = stringResource(R.string.patch_from_applist),\n                            style = MaterialTheme.typography.bodyLarge\n                        )\n                    }\n                }\n            }\n        )\n    }\n\n    FloatingActionButton(\n        content = { Icon(Icons.Filled.Add, stringResource(R.string.add)) },\n        onClick = {\n            val uri = Configs.storageDirectory?.toUri()\n            if (uri == null) {\n                shouldSelectDirectory = true\n            } else {\n                runCatching {\n                    val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION\n                    context.contentResolver.takePersistableUriPermission(uri, takeFlags)\n                    if (DocumentFile.fromTreeUri(context, uri)?.exists() == false) throw IOException(\"Storage directory was deleted\")\n                }.onSuccess {\n                    showNewPatchDialog = true\n                }.onFailure {\n                    Log.w(TAG, \"Failed to take persistable permission for saved uri\", it)\n                    Configs.storageDirectory = null\n                    shouldSelectDirectory = true\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/page/manage/ModuleManagePage.kt",
    "content": "package org.lsposed.lspatch.ui.page.manage\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.provider.Settings\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport org.lsposed.lspatch.R\nimport org.lsposed.lspatch.ui.component.AnywhereDropdown\nimport org.lsposed.lspatch.ui.component.AppItem\nimport org.lsposed.lspatch.ui.viewmodel.manage.ModuleManageViewModel\nimport org.lsposed.lspatch.util.LSPPackageManager\n\n@Composable\nfun ModuleManageBody() {\n    val context = LocalContext.current\n    val viewModel = viewModel<ModuleManageViewModel>()\n    if (viewModel.appList.isEmpty()) {\n        Box(Modifier.fillMaxSize()) {\n            Text(\n                modifier = Modifier.align(Alignment.Center),\n                text = run {\n                    if (LSPPackageManager.appList.isEmpty()) stringResource(R.string.manage_loading)\n                    else stringResource(R.string.manage_no_modules)\n                },\n                fontFamily = FontFamily.Serif,\n                style = MaterialTheme.typography.headlineSmall\n            )\n        }\n    } else {\n        LazyColumn(Modifier.fillMaxHeight()) {\n            items(\n                items = viewModel.appList,\n                key = { it.first.app.packageName }\n            ) {\n                var expanded by remember { mutableStateOf(false) }\n                val settingsIntent = remember { LSPPackageManager.getSettingsIntent(it.first.app.packageName) }\n                AnywhereDropdown(\n                    expanded = expanded,\n                    onDismissRequest = { expanded = false },\n                    onClick = { settingsIntent?.let { context.startActivity(it) } },\n                    onLongClick = { expanded = true },\n                    surface = {\n                        AppItem(\n                            icon = LSPPackageManager.getIcon(it.first),\n                            label = it.first.label,\n                            packageName = it.first.app.packageName,\n                            additionalContent = {\n                                Text(\n                                    text = it.second.description,\n                                    style = MaterialTheme.typography.bodySmall\n                                )\n                                Text(\n                                    text = buildAnnotatedString {\n                                        append(AnnotatedString(\"API\", SpanStyle(color = MaterialTheme.colorScheme.secondary)))\n                                        append(\"  \")\n                                        append(it.second.api.toString())\n                                    },\n                                    fontWeight = FontWeight.SemiBold,\n                                    fontFamily = FontFamily.Serif,\n                                    style = MaterialTheme.typography.bodySmall\n                                )\n                            }\n                        )\n                    }\n                ) {\n                    DropdownMenuItem(\n                        text = { Text(text = it.first.label, color = MaterialTheme.colorScheme.primary) },\n                        onClick = {}, enabled = false\n                    )\n                    if (settingsIntent != null) {\n                        DropdownMenuItem(\n                            text = { Text(stringResource(R.string.manage_module_settings)) },\n                            onClick = { context.startActivity(settingsIntent) }\n                        )\n                    }\n                    DropdownMenuItem(\n                        text = { Text(stringResource(R.string.manage_app_info)) },\n                        onClick = {\n                            val intent = Intent(\n                                Settings.ACTION_APPLICATION_DETAILS_SETTINGS,\n                                Uri.fromParts(\"package\", it.first.app.packageName, null)\n                            )\n                            context.startActivity(intent)\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/theme/Theme.kt",
    "content": "package org.lsposed.lspatch.ui.theme\n\nimport android.app.Activity\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalView\nimport androidx.core.view.ViewCompat\n\n@Composable\nfun LSPTheme(\n    isDarkTheme: Boolean = isSystemInDarkTheme(),\n    enableDynamicColor: Boolean = true,\n    content: @Composable () -> Unit\n) {\n    val colorScheme = when {\n        enableDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n            val context = LocalContext.current\n            if (isDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        }\n        isDarkTheme -> darkColorScheme()\n        else -> lightColorScheme()\n    }\n    val view = LocalView.current\n    if (!view.isInEditMode) {\n        SideEffect {\n            (view.context as Activity).window.statusBarColor = colorScheme.background.toArgb()\n            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = !isDarkTheme\n        }\n    }\n\n    MaterialTheme(\n        colorScheme = colorScheme,\n        typography = Typography,\n        content = content\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/theme/Type.kt",
    "content": "package org.lsposed.lspatch.ui.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.sp\n\nval Typography = Typography(\n    bodyLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 16.sp,\n        lineHeight = 24.sp,\n        letterSpacing = 0.5.sp\n    )\n)\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/util/CompositionProvider.kt",
    "content": "package org.lsposed.lspatch.ui.util\n\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.compositionLocalOf\n\nval LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {\n    error(\"CompositionLocal LocalSnackbarController not present\")\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/util/HtmlText.kt",
    "content": "package org.lsposed.lspatch.ui.util\n\nimport android.text.method.LinkMovementMethod\nimport android.widget.TextView\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.core.text.HtmlCompat\n\n@Composable\nfun HtmlText(html: String, modifier: Modifier = Modifier) {\n    AndroidView(\n        modifier = modifier,\n        factory = { context -> TextView(context) },\n        update = {\n            it.movementMethod = LinkMovementMethod.getInstance()\n            it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)\n        }\n    )\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/util/MultiDelegateState.kt",
    "content": "package org.lsposed.lspatch.ui.util\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport kotlin.reflect.KProperty\n\nclass DelegateState<T>(initial: T, private val sideEffectSetter: (T) -> Unit) {\n    private var snapshot by mutableStateOf(initial)\n\n    var value: T\n        get() = snapshot\n        set(value) {\n            snapshot = value\n            sideEffectSetter(snapshot)\n        }\n\n    operator fun component1(): T = value\n\n    operator fun component2(): (T) -> Unit = { value = it }\n}\n\nfun <T> delegateStateOf(initial: T, sideEffectSetter: (T) -> Unit) = DelegateState(initial, sideEffectSetter)\n\n@Suppress(\"NOTHING_TO_INLINE\")\ninline operator fun <T> DelegateState<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value\n\n@Suppress(\"NOTHING_TO_INLINE\")\ninline operator fun <T> DelegateState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {\n    this.value = value\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/util/Preview.kt",
    "content": "package org.lsposed.lspatch.ui.util\n\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\n\nclass SampleStringProvider : PreviewParameterProvider<String> {\n    override val values: Sequence<String> = sequenceOf(\"Hello\", \"World\")\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/util/Utils.kt",
    "content": "package org.lsposed.lspatch.ui.util\n\nimport androidx.compose.foundation.lazy.LazyListState\n\nval LazyListState.lastVisibleItemIndex\n    get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index\n\nval LazyListState.lastItemIndex\n    get() = layoutInfo.totalItemsCount.let { if (it == 0) null else it }\n\nval LazyListState.isScrolledToEnd\n    get() = lastVisibleItemIndex == lastItemIndex\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/NewPatchViewModel.kt",
    "content": "package org.lsposed.lspatch.ui.viewmodel\n\nimport android.util.Log\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.Patcher\nimport org.lsposed.lspatch.share.PatchConfig\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.LSPPackageManager.AppInfo\nimport org.lsposed.patch.util.Logger\n\nclass NewPatchViewModel : ViewModel() {\n\n    companion object {\n        private const val TAG = \"NewPatchViewModel\"\n    }\n\n    enum class PatchState {\n        INIT, SELECTING, CONFIGURING, PATCHING, FINISHED, ERROR\n    }\n\n    sealed class ViewAction {\n        object DoneInit : ViewAction()\n        data class ConfigurePatch(val app: AppInfo) : ViewAction()\n        object SubmitPatch : ViewAction()\n        object LaunchPatch : ViewAction()\n    }\n\n    var patchState by mutableStateOf(PatchState.INIT)\n        private set\n\n    var useManager by mutableStateOf(true)\n    var debuggable by mutableStateOf(false)\n    var overrideVersionCode by mutableStateOf(false)\n    var sigBypassLevel by mutableStateOf(2)\n    var embeddedModules = emptyList<AppInfo>()\n\n    lateinit var patchApp: AppInfo\n        private set\n    lateinit var patchOptions: Patcher.Options\n        private set\n\n    val logs = mutableStateListOf<Pair<Int, String>>()\n    private val logger = object : Logger() {\n        override fun d(msg: String) {\n            if (verbose) {\n                Log.d(TAG, msg)\n                logs += Log.DEBUG to msg\n            }\n        }\n\n        override fun i(msg: String) {\n            Log.i(TAG, msg)\n            logs += Log.INFO to msg\n        }\n\n        override fun e(msg: String) {\n            Log.e(TAG, msg)\n            logs += Log.ERROR to msg\n        }\n    }\n\n    fun dispatch(action: ViewAction) {\n        viewModelScope.launch {\n            when (action) {\n                is ViewAction.DoneInit -> doneInit()\n                is ViewAction.ConfigurePatch -> configurePatch(action.app)\n                is ViewAction.SubmitPatch -> submitPatch()\n                is ViewAction.LaunchPatch -> launchPatch()\n            }\n        }\n    }\n\n    private fun doneInit() {\n        patchState = PatchState.SELECTING\n    }\n\n    private fun configurePatch(app: AppInfo) {\n        Log.d(TAG, \"Configuring patch for ${app.app.packageName}\")\n        patchApp = app\n        patchState = PatchState.CONFIGURING\n    }\n\n    private fun submitPatch() {\n        Log.d(TAG, \"Submit patch\")\n        if (useManager) embeddedModules = emptyList()\n        patchOptions = Patcher.Options(\n            config = PatchConfig(useManager, debuggable, overrideVersionCode, sigBypassLevel, null, null),\n            apkPaths = listOf(patchApp.app.sourceDir) + (patchApp.app.splitSourceDirs ?: emptyArray()),\n            embeddedModules = embeddedModules.flatMap { listOf(it.app.sourceDir) + (it.app.splitSourceDirs ?: emptyArray()) }\n        )\n        patchState = PatchState.PATCHING\n    }\n\n    private suspend fun launchPatch() {\n        logger.i(\"Launch patch\")\n        patchState = try {\n            Patcher.patch(logger, patchOptions)\n            PatchState.FINISHED\n        } catch (t: Throwable) {\n            logger.e(t.message.orEmpty())\n            logger.e(t.stackTraceToString())\n            PatchState.ERROR\n        } finally {\n            LSPPackageManager.cleanTmpApkDir()\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/SelectAppsViewModel.kt",
    "content": "package org.lsposed.lspatch.ui.viewmodel\n\nimport android.util.Log\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.launch\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.LSPPackageManager.AppInfo\n\nclass SelectAppsViewModel : ViewModel() {\n\n    companion object {\n        private const val TAG = \"SelectAppViewModel\"\n    }\n\n    init {\n        Log.d(TAG, \"SelectAppsViewModel ${toString().substringAfterLast('@')} construct\")\n    }\n\n    var isRefreshing by mutableStateOf(false)\n        private set\n\n    var filteredList by mutableStateOf(listOf<AppInfo>())\n        private set\n\n    val multiSelected = mutableStateListOf<AppInfo>()\n\n    fun filterAppList(refresh: Boolean, filter: (AppInfo) -> Boolean) {\n        viewModelScope.launch {\n            if (LSPPackageManager.appList.isEmpty() || refresh) {\n                isRefreshing = true\n                LSPPackageManager.fetchAppList()\n                isRefreshing = false\n            }\n            filteredList = LSPPackageManager.appList.filter(filter)\n            Log.d(TAG, \"Filtered ${filteredList.size} apps\")\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/AppManageViewModel.kt",
    "content": "package org.lsposed.lspatch.ui.viewmodel.manage\n\nimport android.content.pm.PackageInstaller\nimport android.util.Base64\nimport android.util.Log\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.google.gson.Gson\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.lsposed.lspatch.Patcher\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.share.Constants\nimport org.lsposed.lspatch.share.PatchConfig\nimport org.lsposed.lspatch.ui.viewstate.ProcessingState\nimport org.lsposed.lspatch.util.LSPPackageManager\nimport org.lsposed.lspatch.util.LSPPackageManager.AppInfo\nimport org.lsposed.lspatch.util.ShizukuApi\nimport org.lsposed.patch.util.Logger\nimport java.io.FileNotFoundException\nimport java.util.zip.ZipFile\n\nclass AppManageViewModel : ViewModel() {\n\n    companion object {\n        private const val TAG = \"ManageViewModel\"\n    }\n\n    sealed class ViewAction {\n        data class UpdateLoader(val appInfo: AppInfo, val config: PatchConfig) : ViewAction()\n        object ClearUpdateLoaderResult : ViewAction()\n        data class PerformOptimize(val appInfo: AppInfo) : ViewAction()\n        object ClearOptimizeResult : ViewAction()\n    }\n\n    val appList: List<Pair<AppInfo, PatchConfig>> by derivedStateOf {\n        LSPPackageManager.appList.mapNotNull { appInfo ->\n            appInfo.app.metaData?.getString(\"lspatch\")?.let {\n                val json = Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8)\n                Log.d(TAG, \"Read patched config: $json\")\n                appInfo to Gson().fromJson(json, PatchConfig::class.java)\n            }\n        }.also {\n            Log.d(TAG, \"Loaded ${it.size} patched apps\")\n        }\n    }\n\n    var updateLoaderState: ProcessingState<Result<Unit>> by mutableStateOf(ProcessingState.Idle)\n        private set\n\n    var optimizeState: ProcessingState<Boolean> by mutableStateOf(ProcessingState.Idle)\n        private set\n\n    private val logger = object : Logger() {\n        override fun d(msg: String) {\n            if (verbose) Log.d(TAG, msg)\n        }\n\n        override fun i(msg: String) {\n            Log.i(TAG, msg)\n        }\n\n        override fun e(msg: String) {\n            Log.e(TAG, msg)\n        }\n    }\n\n    fun dispatch(action: ViewAction) {\n        viewModelScope.launch {\n            when (action) {\n                is ViewAction.UpdateLoader -> updateLoader(action.appInfo, action.config)\n                is ViewAction.ClearUpdateLoaderResult -> updateLoaderState = ProcessingState.Idle\n                is ViewAction.PerformOptimize -> performOptimize(action.appInfo)\n                is ViewAction.ClearOptimizeResult -> optimizeState = ProcessingState.Idle\n            }\n        }\n    }\n\n    private suspend fun updateLoader(appInfo: AppInfo, config: PatchConfig) {\n        Log.i(TAG, \"Update loader for ${appInfo.app.packageName}\")\n        updateLoaderState = ProcessingState.Processing\n        val result = runCatching {\n            withContext(Dispatchers.IO) {\n                LSPPackageManager.cleanTmpApkDir()\n                val apkPaths = listOf(appInfo.app.sourceDir) + (appInfo.app.splitSourceDirs ?: emptyArray())\n                val patchPaths = mutableListOf<String>()\n                val embeddedModulePaths = mutableListOf<String>()\n                for (apk in apkPaths) {\n                    ZipFile(apk).use { zip ->\n                        var entry = zip.getEntry(Constants.ORIGINAL_APK_ASSET_PATH)\n                        if (entry == null) entry = zip.getEntry(\"assets/lspatch/origin_apk.bin\")\n                        if (entry == null) throw FileNotFoundException(\"Original apk entry not found for $apk\")\n                        zip.getInputStream(entry).use { input ->\n                            val dst = lspApp.tmpApkDir.resolve(apk.substringAfterLast('/'))\n                            patchPaths.add(dst.absolutePath)\n                            dst.outputStream().use { output ->\n                                input.copyTo(output)\n                            }\n                        }\n                    }\n                }\n                ZipFile(appInfo.app.sourceDir).use { zip ->\n                    zip.entries().iterator().forEach { entry ->\n                        if (entry.name.startsWith(Constants.EMBEDDED_MODULES_ASSET_PATH)) {\n                            val dst = lspApp.tmpApkDir.resolve(entry.name.substringAfterLast('/'))\n                            embeddedModulePaths.add(dst.absolutePath)\n                            zip.getInputStream(entry).use { input ->\n                                dst.outputStream().use { output ->\n                                    input.copyTo(output)\n                                }\n                            }\n                        }\n                    }\n                }\n                Patcher.patch(logger, Patcher.Options(config, patchPaths, embeddedModulePaths))\n                val (status, message) = LSPPackageManager.install()\n                if (status != PackageInstaller.STATUS_SUCCESS) throw RuntimeException(message)\n            }\n        }\n        updateLoaderState = ProcessingState.Done(result)\n    }\n\n    private suspend fun performOptimize(appInfo: AppInfo) {\n        Log.i(TAG, \"Perform optimize for ${appInfo.app.packageName}\")\n        optimizeState = ProcessingState.Processing\n        val result = withContext(Dispatchers.IO) {\n            ShizukuApi.performDexOptMode(appInfo.app.packageName)\n        }\n        optimizeState = ProcessingState.Done(result)\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/viewmodel/manage/ModuleManageViewModel.kt",
    "content": "package org.lsposed.lspatch.ui.viewmodel.manage\n\nimport android.util.Log\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.lifecycle.ViewModel\nimport org.lsposed.lspatch.util.LSPPackageManager\n\nclass ModuleManageViewModel : ViewModel() {\n\n    companion object {\n        private const val TAG = \"ModuleManageViewModel\"\n    }\n\n    class XposedInfo(\n        val api: Int,\n        val description: String,\n        val scope: List<String>\n    )\n\n    val appList: List<Pair<LSPPackageManager.AppInfo, XposedInfo>> by derivedStateOf {\n        LSPPackageManager.appList.mapNotNull { appInfo ->\n            val metaData = appInfo.app.metaData ?: return@mapNotNull null\n            appInfo to XposedInfo(\n                metaData.getInt(\"xposedminversion\", -1).also { if (it == -1) return@mapNotNull null },\n                metaData.getString(\"xposeddescription\") ?: \"\",\n                emptyList() // TODO: scope\n            )\n        }.also {\n            Log.d(TAG, \"Loaded ${it.size} Xposed modules\")\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/ui/viewstate/ProcessingState.kt",
    "content": "package org.lsposed.lspatch.ui.viewstate\n\nsealed class ProcessingState<out T> {\n    object Idle : ProcessingState<Nothing>()\n    object Processing : ProcessingState<Nothing>()\n    data class Done<T>(val result: T) : ProcessingState<T>()\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/util/IntentSenderHelper.kt",
    "content": "package org.lsposed.lspatch.util\n\nimport android.content.IIntentReceiver\nimport android.content.IIntentSender\nimport android.content.Intent\nimport android.content.IntentSender\nimport android.os.Bundle\nimport android.os.IBinder\n\nobject IntentSenderHelper {\n\n    fun newIntentSender(binder: IIntentSender): IntentSender {\n        return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(binder)\n    }\n\n    class IIntentSenderAdaptor(private val listener: (Intent) -> Unit) : IIntentSender.Stub() {\n        override fun send(\n            code: Int,\n            intent: Intent,\n            resolvedType: String?,\n            finishedReceiver: IIntentReceiver?,\n            requiredPermission: String?,\n            options: Bundle?\n        ): Int {\n            listener(intent)\n            return 0\n        }\n\n        override fun send(\n            code: Int,\n            intent: Intent,\n            resolvedType: String?,\n            whitelistToken: IBinder?,\n            finishedReceiver: IIntentReceiver?,\n            requiredPermission: String?,\n            options: Bundle?\n        ) {\n            listener(intent)\n        }\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/util/LSPPackageManager.kt",
    "content": "package org.lsposed.lspatch.util\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageInstaller\nimport android.content.pm.PackageInstallerHidden.SessionParamsHidden\nimport android.content.pm.PackageManager\nimport android.content.pm.PackageManagerHidden\nimport android.net.Uri\nimport android.os.Parcelable\nimport android.util.Log\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.core.net.toUri\nimport androidx.documentfile.provider.DocumentFile\nimport dev.rikka.tools.refine.Refine\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.parcelize.Parcelize\nimport me.zhanghai.android.appiconloader.AppIconLoader\nimport org.lsposed.lspatch.config.ConfigManager\nimport org.lsposed.lspatch.config.Configs\nimport org.lsposed.lspatch.lspApp\nimport org.lsposed.lspatch.share.Constants\nimport java.io.File\nimport java.io.IOException\nimport java.text.Collator\nimport java.util.*\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\nobject LSPPackageManager {\n\n    private const val TAG = \"LSPPackageManager\"\n    private const val SETTINGS_CATEGORY = \"de.robv.android.xposed.category.MODULE_SETTINGS\"\n\n    const val STATUS_USER_CANCELLED = -2\n\n    @Parcelize\n    class AppInfo(val app: ApplicationInfo, val label: String) : Parcelable {\n        val isXposedModule: Boolean\n            get() = app.metaData?.get(\"xposedminversion\") != null\n    }\n\n    var appList by mutableStateOf(listOf<AppInfo>())\n        private set\n\n    @SuppressLint(\"StaticFieldLeak\")\n    private val iconLoader = AppIconLoader(lspApp.resources.getDimensionPixelSize(android.R.dimen.app_icon_size), false, lspApp)\n    private val appIcon = mutableMapOf<String, ImageBitmap>()\n\n    suspend fun fetchAppList() {\n        withContext(Dispatchers.IO) {\n            val pm = lspApp.packageManager\n            val collection = mutableListOf<AppInfo>()\n            pm.getInstalledApplications(PackageManager.GET_META_DATA).forEach {\n                val label = pm.getApplicationLabel(it)\n                collection.add(AppInfo(it, label.toString()))\n                appIcon[it.packageName] = iconLoader.loadIcon(it).asImageBitmap()\n            }\n            collection.sortWith(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))\n            val modules = buildMap {\n                collection.forEach { if (it.isXposedModule) put(it.app.packageName, it.app.sourceDir) }\n            }\n            ConfigManager.updateModules(modules)\n            appList = collection\n        }\n    }\n\n    fun getIcon(appInfo: AppInfo) = appIcon[appInfo.app.packageName]!!\n\n    suspend fun cleanTmpApkDir() {\n        withContext(Dispatchers.IO) {\n            lspApp.tmpApkDir.listFiles()?.forEach(File::delete)\n        }\n    }\n\n    suspend fun install(): Pair<Int, String?> {\n        Log.i(TAG, \"Perform install patched apks\")\n        var status = PackageInstaller.STATUS_FAILURE\n        var message: String? = null\n        withContext(Dispatchers.IO) {\n            runCatching {\n                val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)\n                var flags = Refine.unsafeCast<SessionParamsHidden>(params).installFlags\n                flags = flags or PackageManagerHidden.INSTALL_ALLOW_TEST or PackageManagerHidden.INSTALL_REPLACE_EXISTING\n                Refine.unsafeCast<SessionParamsHidden>(params).installFlags = flags\n                ShizukuApi.createPackageInstallerSession(params).use { session ->\n                    val uri = Configs.storageDirectory?.toUri() ?: throw IOException(\"Uri is null\")\n                    val root = DocumentFile.fromTreeUri(lspApp, uri) ?: throw IOException(\"DocumentFile is null\")\n                    root.listFiles().forEach { file ->\n                        if (file.name?.endsWith(Constants.PATCH_FILE_SUFFIX) != true) return@forEach\n                        Log.d(TAG, \"Add ${file.name}\")\n                        val input = lspApp.contentResolver.openInputStream(file.uri)\n                            ?: throw IOException(\"Cannot open input stream\")\n                        input.use {\n                            session.openWrite(file.name!!, 0, input.available().toLong()).use { output ->\n                                input.copyTo(output)\n                                session.fsync(output)\n                            }\n                        }\n                    }\n                    var result: Intent? = null\n                    suspendCoroutine { cont ->\n                        val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent ->\n                            result = intent\n                            cont.resume(Unit)\n                        }\n                        val intentSender = IntentSenderHelper.newIntentSender(adapter)\n                        session.commit(intentSender)\n                    }\n                    result?.let {\n                        status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)\n                        message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)\n                    } ?: throw IOException(\"Intent is null\")\n                }\n            }.onFailure {\n                status = PackageInstaller.STATUS_FAILURE\n                message = it.message + \"\\n\" + it.stackTraceToString()\n            }\n        }\n        return Pair(status, message)\n    }\n\n    suspend fun uninstall(packageName: String): Pair<Int, String?> {\n        var status = PackageInstaller.STATUS_FAILURE\n        var message: String? = null\n        withContext(Dispatchers.IO) {\n            runCatching {\n                var result: Intent? = null\n                suspendCoroutine { cont ->\n                    val adapter = IntentSenderHelper.IIntentSenderAdaptor { intent ->\n                        result = intent\n                        cont.resume(Unit)\n                    }\n                    val intentSender = IntentSenderHelper.newIntentSender(adapter)\n                    ShizukuApi.uninstallPackage(packageName, intentSender)\n                }\n                result?.let {\n                    status = it.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)\n                    message = it.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)\n                } ?: throw IOException(\"Intent is null\")\n            }.onFailure {\n                status = PackageInstaller.STATUS_FAILURE\n                message = \"Exception happened\\n$it\"\n            }\n        }\n        return Pair(status, message)\n    }\n\n    suspend fun getAppInfoFromApks(apks: List<Uri>): Result<List<AppInfo>> {\n        return withContext(Dispatchers.IO) {\n            runCatching {\n                var primary: ApplicationInfo? = null\n                val splits = mutableListOf<String>()\n                val appInfos = apks.mapNotNull { uri ->\n                    val src = DocumentFile.fromSingleUri(lspApp, uri)\n                        ?: throw IOException(\"DocumentFile is null\")\n                    val dst = lspApp.tmpApkDir.resolve(src.name!!)\n                    val input = lspApp.contentResolver.openInputStream(uri)\n                        ?: throw IOException(\"InputStream is null\")\n                    input.use {\n                        dst.outputStream().use { output ->\n                            input.copyTo(output)\n                        }\n                    }\n\n                    val appInfo = lspApp.packageManager.getPackageArchiveInfo(\n                        dst.absolutePath, PackageManager.GET_META_DATA\n                    )?.applicationInfo\n                    appInfo?.sourceDir = dst.absolutePath\n                    if (appInfo == null) {\n                        splits.add(dst.absolutePath)\n                        return@mapNotNull null\n                    }\n                    if (primary == null) {\n                        primary = appInfo\n                    }\n                    val label = lspApp.packageManager.getApplicationLabel(appInfo).toString()\n                    AppInfo(appInfo, label)\n                }\n                // TODO: Check selected apks are from the same app\n                primary?.splitSourceDirs = splits.toTypedArray()\n                if (appInfos.isEmpty()) throw IOException(\"No apks\")\n                appInfos\n            }.recoverCatching { t ->\n                cleanTmpApkDir()\n                Log.e(TAG, \"Failed to load apks\", t)\n                throw t\n            }\n        }\n    }\n\n    fun getLaunchIntentForPackage(packageName: String): Intent? {\n        val intentToResolve = Intent(Intent.ACTION_MAIN)\n        intentToResolve.addCategory(Intent.CATEGORY_INFO)\n        intentToResolve.setPackage(packageName)\n        var ris = lspApp.packageManager.queryIntentActivities(intentToResolve, 0)\n\n        if (ris.size <= 0) {\n            intentToResolve.removeCategory(Intent.CATEGORY_INFO)\n            intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER)\n            intentToResolve.setPackage(packageName)\n            ris = lspApp.packageManager.queryIntentActivities(intentToResolve, 0)\n        }\n\n        if (ris.size <= 0) return null\n\n        return Intent(intentToResolve)\n            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            .setClassName(\n                ris[0].activityInfo.packageName,\n                ris[0].activityInfo.name\n            )\n    }\n\n    fun getSettingsIntent(packageName: String): Intent? {\n        val intentToResolve = Intent(Intent.ACTION_MAIN)\n        intentToResolve.addCategory(SETTINGS_CATEGORY)\n        intentToResolve.setPackage(packageName)\n        val ris = lspApp.packageManager.queryIntentActivities(intentToResolve, 0)\n\n        if (ris.size <= 0) return getLaunchIntentForPackage(packageName)\n\n        return Intent(intentToResolve)\n            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            .setClassName(\n                ris[0].activityInfo.packageName,\n                ris[0].activityInfo.name\n            )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt",
    "content": "package org.lsposed.lspatch.util\n\nimport android.content.IntentSender\nimport android.content.pm.*\nimport android.os.Build\nimport android.os.IBinder\nimport android.os.IInterface\nimport android.os.Process\nimport android.os.SystemProperties\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport dev.rikka.tools.refine.Refine\nimport rikka.shizuku.Shizuku\nimport rikka.shizuku.ShizukuBinderWrapper\nimport rikka.shizuku.SystemServiceHelper\n\nobject ShizukuApi {\n\n    private fun IBinder.wrap() = ShizukuBinderWrapper(this)\n    private fun IInterface.asShizukuBinder() = this.asBinder().wrap()\n\n    private val iPackageManager: IPackageManager by lazy {\n        IPackageManager.Stub.asInterface(SystemServiceHelper.getSystemService(\"package\").wrap())\n    }\n\n    private val iPackageInstaller: IPackageInstaller by lazy {\n        IPackageInstaller.Stub.asInterface(iPackageManager.packageInstaller.asShizukuBinder())\n    }\n\n    private val packageInstaller: PackageInstaller by lazy {\n        val userId = Process.myUserHandle().hashCode()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            Refine.unsafeCast(PackageInstallerHidden(iPackageInstaller, \"com.android.shell\", null, userId))\n        } else {\n            Refine.unsafeCast(PackageInstallerHidden(iPackageInstaller, \"com.android.shell\", userId))\n        }\n    }\n\n    var isBinderAvailable = false\n    var isPermissionGranted by mutableStateOf(false)\n\n    fun init() {\n        Shizuku.addBinderReceivedListenerSticky {\n            isBinderAvailable = true\n            isPermissionGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED\n        }\n        Shizuku.addBinderDeadListener {\n            isBinderAvailable = false\n            isPermissionGranted = false\n        }\n    }\n\n    fun createPackageInstallerSession(params: PackageInstaller.SessionParams): PackageInstaller.Session {\n        val sessionId = packageInstaller.createSession(params)\n        val iSession = IPackageInstallerSession.Stub.asInterface(iPackageInstaller.openSession(sessionId).asShizukuBinder())\n        return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession))\n    }\n\n    fun isPackageInstalledWithoutPatch(packageName: String): Boolean {\n        val userId = Process.myUserHandle().hashCode()\n        val app = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA.toLong(), userId)\n        } else {\n            iPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId)\n        }\n        return (app != null) && (app.metaData?.containsKey(\"lspatch\") != true)\n    }\n\n    fun uninstallPackage(packageName: String, intentSender: IntentSender) {\n        packageInstaller.uninstall(packageName, intentSender)\n    }\n\n    fun performDexOptMode(packageName: String): Boolean {\n        return iPackageManager.performDexOptMode(\n            packageName,\n            SystemProperties.getBoolean(\"dalvik.vm.usejitprofiles\", false),\n            \"verify\", true, true, null\n        )\n    }\n}\n"
  },
  {
    "path": "manager/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ This file is part of LSPosed.\n  ~\n  ~ LSPosed is free software: you can redistribute it and/or modify\n  ~ it under the terms of the GNU General Public License as published by\n  ~ the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ LSPosed is distributed in the hope that it will be useful,\n  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of\n  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n  ~ GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License\n  ~ along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n  ~\n  ~ Copyright (C) 2022 LSPosed Contributors\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    <group\n        android:scaleX=\"0.28265625\"\n        android:scaleY=\"0.28265625\"\n        android:translateX=\"17.82\"\n        android:translateY=\"17.82\">\n        <path\n            android:fillColor=\"#c9dc87\"\n            android:pathData=\"M0,0h256v256h-256z\" />\n        <path\n            android:fillAlpha=\"0.7\"\n            android:fillColor=\"#fff\"\n            android:pathData=\"M256,256v-27.38c-26.01,-15.34 -73.6,-25.62 -128,-25.62S26.01,213.28 0,228.62v27.38H256Z\"\n            android:strokeAlpha=\"0.7\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M78.19,218.79h6.13c3.84,0 6.69,1.36 6.69,5 0,5.12 -4.17,7.38 -8.73,7.38h-2.48l-1.42,7.17h-4.11l3.91,-19.55ZM82.56,227.91c2.91,0 4.39,-1.4 4.39,-3.52 0,-1.64 -1.16,-2.35 -3.26,-2.35h-2.06l-1.15,5.88h2.08ZM81.88,230.21l2.97,-2.63 4.22,10.76h-4.33l-2.85,-8.12Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M91.9,232.57c0,-5.74 4.24,-9.47 8.34,-9.47 3.47,0 5.78,2.44 5.78,6.13 0,5.74 -4.24,9.47 -8.34,9.47 -3.47,0 -5.78,-2.44 -5.78,-6.13ZM101.87,229.32c0,-1.82 -0.7,-2.91 -2.08,-2.91 -2,0 -3.73,2.62 -3.73,6.09 0,1.82 0.7,2.91 2.08,2.91 2,0 3.73,-2.61 3.73,-6.09Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M107.89,232.57c0,-5.74 4.24,-9.47 8.34,-9.47 3.47,0 5.78,2.44 5.78,6.13 0,5.74 -4.24,9.47 -8.34,9.47 -3.47,0 -5.78,-2.44 -5.78,-6.13ZM117.86,229.32c0,-1.82 -0.7,-2.91 -2.08,-2.91 -2,0 -3.73,2.62 -3.73,6.09 0,1.82 0.7,2.91 2.08,2.91 2,0 3.73,-2.61 3.73,-6.09Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M124.86,234.76c0,-0.64 0.11,-1.25 0.22,-1.87l1.3,-6.23h-1.98l0.61,-3.03 2.19,-0.17 1.26,-3.89h3.44l-0.74,3.89h3.37l-0.62,3.2h-3.47l-1.3,6.39c-0.06,0.36 -0.07,0.67 -0.07,0.99 0,0.98 0.48,1.46 1.56,1.46 0.42,0 0.83,-0.15 1.21,-0.34l0.75,2.88c-0.79,0.3 -1.96,0.66 -3.49,0.66 -3,0 -4.21,-1.66 -4.21,-3.94Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M135.1,235.79c0,-0.53 0.06,-1.07 0.22,-1.82l3.33,-16.65h4.1l-3.38,16.81c-0.06,0.3 -0.06,0.43 -0.06,0.57 0,0.51 0.27,0.69 0.57,0.69 0.17,0 0.28,0 0.53,-0.07l-0.13,3.04c-0.5,0.17 -1.22,0.33 -2.17,0.33 -2.13,0 -3.01,-1.1 -3.01,-2.91Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M151.39,223.1c3.55,0 4.85,2.49 4.85,5.58 0,1.4 -0.53,2.94 -0.76,3.39h-8.48c-0.1,2.48 1.39,3.53 3.35,3.53 0.92,0 2,-0.51 2.73,-1.04l1.46,2.59c-1.19,0.84 -3.04,1.56 -5.34,1.56 -3.6,0 -6.09,-2.39 -6.09,-6.38 0,-5.51 4.31,-9.23 8.27,-9.23ZM152.75,229.56c0.07,-0.3 0.13,-0.7 0.13,-1.11 0,-1.2 -0.5,-2.2 -2,-2.2 -1.42,0 -2.87,1.12 -3.55,3.31h5.42Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M156.88,235.81l2.39,-1.96c1.03,1.26 2.13,1.85 3.21,1.85s2.07,-0.61 2.07,-1.41c0,-0.85 -0.87,-1.24 -2.56,-2.2 -1.69,-0.95 -3.01,-2.25 -3.01,-4.14 0,-2.79 2.58,-4.85 5.76,-4.85 2.05,0 3.67,1.01 4.88,2.27l-2.23,2.13c-0.73,-0.74 -1.6,-1.35 -2.66,-1.35 -1.13,0 -1.9,0.64 -1.9,1.42 0,0.95 1.26,1.38 2.45,2.06 1.77,0.96 3.12,2.14 3.12,4.18 0,2.93 -2.59,4.89 -6.19,4.89 -1.8,0 -4.11,-1.04 -5.33,-2.89Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M169.6,235.81l2.39,-1.96c1.03,1.26 2.13,1.85 3.21,1.85s2.07,-0.61 2.07,-1.41c0,-0.85 -0.87,-1.24 -2.56,-2.2 -1.69,-0.95 -3.01,-2.25 -3.01,-4.14 0,-2.79 2.58,-4.85 5.76,-4.85 2.05,0 3.67,1.01 4.88,2.27l-2.23,2.13c-0.73,-0.74 -1.6,-1.35 -2.66,-1.35 -1.13,0 -1.9,0.64 -1.9,1.42 0,0.95 1.26,1.38 2.45,2.06 1.77,0.96 3.12,2.14 3.12,4.18 0,2.93 -2.59,4.89 -6.19,4.89 -1.8,0 -4.11,-1.04 -5.33,-2.89Z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "manager/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ This file is part of LSPosed.\n  ~\n  ~ LSPosed is free software: you can redistribute it and/or modify\n  ~ it under the terms of the GNU General Public License as published by\n  ~ the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ LSPosed is distributed in the hope that it will be useful,\n  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of\n  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n  ~ GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License\n  ~ along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n  ~\n  ~ Copyright (C) 2022 LSPosed Contributors\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    <group\n        android:scaleX=\"0.27421874\"\n        android:scaleY=\"0.27421874\"\n        android:translateX=\"18.9\"\n        android:translateY=\"18.9\">\n        <path\n            android:fillColor=\"#fff\"\n            android:pathData=\"M167.13,107.36l27.34,-27.34c2.65,-2.67 2.65,-6.97 0,-9.64l-29.8,-29.39c-2.67,-2.65 -6.97,-2.65 -9.64,0l-27.34,27.34 -27.34,-27.34c-1.22,-1.21 -2.86,-1.92 -4.58,-1.98 -1.79,0 -3.51,0.72 -4.79,1.98l-29.67,29.67c-2.47,2.63 -2.47,6.73 0,9.37l27.34,27.34 -27.34,27.34c-2.65,2.67 -2.65,6.97 0,9.64l29.67,29.67c2.67,2.65 6.97,2.65 9.64,0l27.34,-27.34 27.34,27.34c1.29,1.28 3.04,1.99 4.85,1.98 1.82,0.01 3.56,-0.7 4.85,-1.98l29.67,-29.67c2.65,-2.67 2.65,-6.97 0,-9.64l-27.55,-27.34ZM127.69,86.85c3.78,0 6.84,3.06 6.84,6.84s-3.06,6.84 -6.84,6.84 -6.84,-3.06 -6.84,-6.84 3.06,-6.84 6.84,-6.84ZM95.77,100.52l-24.81,-25.02 24.81,-24.81 25.09,24.75 -25.09,25.09ZM114.02,114.19c-3.78,0 -6.84,-3.06 -6.84,-6.84s3.06,-6.84 6.84,-6.84 6.84,3.06 6.84,6.84 -3.06,6.84 -6.84,6.84ZM127.69,127.86c-3.78,0 -6.84,-3.06 -6.84,-6.84s3.06,-6.84 6.84,-6.84 6.84,3.06 6.84,6.84 -3.06,6.84 -6.84,6.84ZM141.36,100.52c3.78,0 6.84,3.06 6.84,6.84s-3.06,6.84 -6.84,6.84 -6.84,-3.06 -6.84,-6.84 3.06,-6.84 6.84,-6.84ZM159.54,164.37l-24.81,-24.75 24.81,-24.81 24.75,24.75 -24.75,24.81Z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "manager/src/main/res/drawable-zh-rCN/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ This file is part of LSPosed.\n  ~\n  ~ LSPosed is free software: you can redistribute it and/or modify\n  ~ it under the terms of the GNU General Public License as published by\n  ~ the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ LSPosed is distributed in the hope that it will be useful,\n  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of\n  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n  ~ GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License\n  ~ along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n  ~\n  ~ Copyright (C) 2022 LSPosed Contributors\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    <group\n        android:scaleX=\"0.28265625\"\n        android:scaleY=\"0.28265625\"\n        android:translateX=\"17.82\"\n        android:translateY=\"17.82\">\n        <path\n            android:fillColor=\"#c9dc87\"\n            android:pathData=\"M0,0h256v256h-256z\" />\n        <path\n            android:fillAlpha=\"0.7\"\n            android:fillColor=\"#fff\"\n            android:pathData=\"M256,256v-27.38c-26.01,-15.34 -73.6,-25.62 -128,-25.62S26.01,213.28 0,228.62v27.38H256Z\"\n            android:strokeAlpha=\"0.7\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M86.37,232.77l1.67,-9.49 -3.13,2.46 -0.29,-0.35c1.25,-1.33 2.51,-2.73 3.79,-4.22l0.08,-0.47 0.21,0.12c2.17,-2.58 4.29,-5.27 6.35,-8.09l2.22,2.7c-0.57,0.55 -1.16,1.05 -1.79,1.52h7.03c0.38,-0.39 0.76,-0.78 1.14,-1.17l1.85,2.81c-2.03,1.33 -4.16,2.54 -6.38,3.63h6.21l2.92,-1.29 -1.98,11.25 -2.17,-2.34h-4.22l0.89,0.94 -1.24,7.03h2.58l-0.02,0.12h3.4l4.53,-6.45 0.6,0.59 -5.6,7.85 -0.1,-0.12 -0.02,0.12h-4.92l0.02,-0.12c-1.53,0.23 -2.84,1.05 -3.95,2.46l0.43,-2.46 -0.14,0.12 1.78,-10.08h-0.23c-0.29,1.64 -1.16,3.01 -2.6,4.1 -1.85,1.64 -3.95,3.13 -6.29,4.45 -2.34,1.33 -4.62,2.5 -6.83,3.52l-0.23,-0.7c1.9,-1.02 3.96,-2.29 6.18,-3.81 2.22,-1.52 4.06,-3.07 5.5,-4.63 0.78,-0.86 1.3,-1.83 1.57,-2.93h-5.62l-0.17,0.94 -3.05,1.99ZM89.81,228.55h5.62l0.89,-5.04h-5.62l-0.89,5.04ZM90.93,222.22h6.21c1.41,-1.33 2.77,-2.66 4.1,-3.98h-7.27c-1.3,1.17 -2.63,2.31 -4,3.4l0.95,0.59ZM98.69,225.38l-0.56,3.16h0.23l0.04,-0.23 0.19,0.23h5.62l0.89,-5.04h-8.09l1.66,1.88Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M116.29,218.38h7.04c4.15,0 7.29,1.44 6.47,6.09 -0.79,4.49 -4.53,6.43 -8.68,6.43h-2.62l-1.24,7.04h-4.42l3.45,-19.55ZM121.38,227.38c2.33,0 3.75,-1 4.09,-2.92 0.34,-1.93 -0.79,-2.58 -3.12,-2.58h-2.26l-0.97,5.5h2.26ZM120.52,229.72l3.56,-2.83 4.29,11.03h-4.95l-2.9,-8.2Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M131.02,230.49c0.87,-4.95 4.86,-7.81 8.63,-7.81s6.75,2.86 5.87,7.81c-0.87,4.94 -4.86,7.79 -8.62,7.79s-6.75,-2.86 -5.88,-7.79ZM141,230.49c0.45,-2.58 -0.16,-4.25 -1.98,-4.25s-3.03,1.67 -3.48,4.25c-0.45,2.58 0.16,4.24 1.98,4.24s3.02,-1.66 3.48,-4.24Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M147.67,230.49c0.87,-4.95 4.86,-7.81 8.63,-7.81s6.75,2.86 5.87,7.81c-0.87,4.94 -4.86,7.79 -8.62,7.79s-6.75,-2.86 -5.88,-7.79ZM157.65,230.49c0.45,-2.58 -0.16,-4.25 -1.98,-4.25s-3.03,1.67 -3.48,4.25c-0.45,2.58 0.16,4.24 1.98,4.24s3.02,-1.66 3.48,-4.24Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M164.64,232.52l1.06,-6.01h-2.03l0.58,-3.29 2.31,-0.17 1.21,-3.95h3.65l-0.7,3.95h3.57l-0.61,3.46h-3.57l-1.05,5.96c-0.3,1.69 0.35,2.36 1.51,2.36 0.49,0 1.05,-0.14 1.46,-0.28l0.13,3.2c-0.83,0.25 -1.96,0.54 -3.4,0.54 -3.68,0 -4.73,-2.32 -4.12,-5.77Z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "manager/src/main/res/drawable-zh-rTW/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ This file is part of LSPosed.\n  ~\n  ~ LSPosed is free software: you can redistribute it and/or modify\n  ~ it under the terms of the GNU General Public License as published by\n  ~ the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ LSPosed is distributed in the hope that it will be useful,\n  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of\n  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n  ~ GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License\n  ~ along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n  ~\n  ~ Copyright (C) 2022 LSPosed Contributors\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    <group\n        android:scaleX=\"0.28265625\"\n        android:scaleY=\"0.28265625\"\n        android:translateX=\"17.82\"\n        android:translateY=\"17.82\">\n        <path\n            android:fillColor=\"#c9dc87\"\n            android:pathData=\"M0,0h256v256h-256z\" />\n        <path\n            android:fillAlpha=\"0.7\"\n            android:fillColor=\"#fff\"\n            android:pathData=\"M256,256v-27.38c-26.01,-15.34 -73.6,-25.62 -128,-25.62S26.01,213.28 0,228.62v27.38H256Z\"\n            android:strokeAlpha=\"0.7\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M86.37,232.77l1.67,-9.49 -3.13,2.46 -0.29,-0.35c1.25,-1.33 2.51,-2.73 3.79,-4.22l0.08,-0.47 0.21,0.12c2.17,-2.58 4.29,-5.27 6.35,-8.09l2.22,2.7c-0.57,0.55 -1.16,1.05 -1.79,1.52h7.03c0.38,-0.39 0.76,-0.78 1.14,-1.17l1.85,2.81c-2.03,1.33 -4.16,2.54 -6.38,3.63h6.21l2.92,-1.29 -1.98,11.25 -2.17,-2.34h-4.22l0.89,0.94 -1.24,7.03h2.58l-0.02,0.12h3.4l4.53,-6.45 0.6,0.59 -5.6,7.85 -0.1,-0.12 -0.02,0.12h-4.92l0.02,-0.12c-1.53,0.23 -2.84,1.05 -3.95,2.46l0.43,-2.46 -0.14,0.12 1.78,-10.08h-0.23c-0.29,1.64 -1.16,3.01 -2.6,4.1 -1.85,1.64 -3.95,3.13 -6.29,4.45 -2.34,1.33 -4.62,2.5 -6.83,3.52l-0.23,-0.7c1.9,-1.02 3.96,-2.29 6.18,-3.81 2.22,-1.52 4.06,-3.07 5.5,-4.63 0.78,-0.86 1.3,-1.83 1.57,-2.93h-5.62l-0.17,0.94 -3.05,1.99ZM89.81,228.55h5.62l0.89,-5.04h-5.62l-0.89,5.04ZM90.93,222.22h6.21c1.41,-1.33 2.77,-2.66 4.1,-3.98h-7.27c-1.3,1.17 -2.63,2.31 -4,3.4l0.95,0.59ZM98.69,225.38l-0.56,3.16h0.23l0.04,-0.23 0.19,0.23h5.62l0.89,-5.04h-8.09l1.66,1.88Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M116.29,218.38h7.04c4.15,0 7.29,1.44 6.47,6.09 -0.79,4.49 -4.53,6.43 -8.68,6.43h-2.62l-1.24,7.04h-4.42l3.45,-19.55ZM121.38,227.38c2.33,0 3.75,-1 4.09,-2.92 0.34,-1.93 -0.79,-2.58 -3.12,-2.58h-2.26l-0.97,5.5h2.26ZM120.52,229.72l3.56,-2.83 4.29,11.03h-4.95l-2.9,-8.2Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M131.02,230.49c0.87,-4.95 4.86,-7.81 8.63,-7.81s6.75,2.86 5.87,7.81c-0.87,4.94 -4.86,7.79 -8.62,7.79s-6.75,-2.86 -5.88,-7.79ZM141,230.49c0.45,-2.58 -0.16,-4.25 -1.98,-4.25s-3.03,1.67 -3.48,4.25c-0.45,2.58 0.16,4.24 1.98,4.24s3.02,-1.66 3.48,-4.24Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M147.67,230.49c0.87,-4.95 4.86,-7.81 8.63,-7.81s6.75,2.86 5.87,7.81c-0.87,4.94 -4.86,7.79 -8.62,7.79s-6.75,-2.86 -5.88,-7.79ZM157.65,230.49c0.45,-2.58 -0.16,-4.25 -1.98,-4.25s-3.03,1.67 -3.48,4.25c-0.45,2.58 0.16,4.24 1.98,4.24s3.02,-1.66 3.48,-4.24Z\" />\n        <path\n            android:fillColor=\"#0e7c61\"\n            android:pathData=\"M164.64,232.52l1.06,-6.01h-2.03l0.58,-3.29 2.31,-0.17 1.21,-3.95h3.65l-0.7,3.95h3.57l-0.61,3.46h-3.57l-1.05,5.96c-0.3,1.69 0.35,2.36 1.51,2.36 0.49,0 1.05,-0.14 1.46,-0.28l0.13,3.2c-0.83,0.25 -1.96,0.54 -3.4,0.54 -3.68,0 -4.73,-2.32 -4.12,-5.77Z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "manager/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=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "manager/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"add\">Add</string>\n    <string name=\"install\">Install</string>\n    <string name=\"installing\">Installing</string>\n    <string name=\"uninstall\">Uninstall</string>\n    <string name=\"uninstalling\">Uninstalling</string>\n    <string name=\"copy_error\">Copy error</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Modules</string>\n    <string name=\"shizuku_available\">Shizuku service available</string>\n    <string name=\"shizuku_unavailable\">Shizuku service not connected</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Logs</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Unknown error</string>\n\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Some functions unavailable</string>\n    <string name=\"home_api_version\">API Version</string>\n    <string name=\"home_lspatch_version\">LSPatch Version</string>\n    <string name=\"home_framework_version\">Framework Version</string>\n    <string name=\"home_system_version\">System Version</string>\n    <string name=\"home_device\">Device</string>\n    <string name=\"home_system_abi\">System ABI</string>\n    <string name=\"home_info_copied\">Copied to clipboard</string>\n    <string name=\"home_support\">Support</string>\n    <string name=\"home_description\">LSPatch is a free non-root Xposed framework based on LSPosed core.</string>\n    <string name=\"home_view_source_code\"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>\n\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Manage</string>\n    <string name=\"manage_loading\">Loading</string>\n    <string name=\"manage_no_apps\">No patched apps yet</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Update loader</string>\n    <string name=\"manage_update_loader_successfully\">Update successfully</string>\n    <string name=\"manage_update_loader_failed\">Update failed</string>\n    <string name=\"manage_module_scope\">Module scope</string>\n    <string name=\"manage_optimize\">Optimize</string>\n    <string name=\"manage_optimize_successfully\">Optimize successfully</string>\n    <string name=\"manage_optimize_failed\">Optimize failed</string>\n    <string name=\"manage_uninstall_successfully\">Uninstall successfully</string>\n    <string name=\"manage_no_modules\">No modules yet</string>\n    <string name=\"manage_module_settings\">Module settings</string>\n    <string name=\"manage_app_info\">App info</string>\n\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">New Patch</string>\n    <string name=\"patch_select_dir_title\">Select storage directory</string>\n    <string name=\"patch_select_dir_text\">Select a directory to store the patched apks</string>\n    <string name=\"patch_select_dir_error\">Error when setting storage directory</string>\n    <string name=\"patch_from_storage\">Select apk(s) from storage</string>\n    <string name=\"patch_from_applist\">Select an installed app</string>\n    <string name=\"patch_mode\">Patch Mode</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Patch an app without modules embedded.\\nXposed scope can be changed dynamically without re-patch.\\nLocal patched apps can only run on the local device.</string>\n    <string name=\"patch_integrated\">Integrated</string>\n    <string name=\"patch_integrated_desc\">Patch an app with modules embedded.\\nThe patched app can run without the manager, but cannot be managed dynamically.\\nIntegrated patched apps can be used on devices that do not have LSPatch Manager installed.</string>\n    <string name=\"patch_embed_modules\">Embed modules</string>\n    <string name=\"patch_debuggable\">Debuggable</string>\n    <string name=\"patch_sigbypass\">Signature bypass</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Off</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Bypass PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Override version code</string>\n    <string name=\"patch_override_version_code_desc\">Override the patched app\\'s version code to 1\\nThis allows downgrade installation in the future, and generally this will not affect the version code actually perceived by the application</string>\n    <string name=\"patch_start\">Start Patch</string>\n    <string name=\"patch_return\">Return</string>\n    <string name=\"patch_uninstall_text\">Due to different signatures, you need to uninstall the original app before installing the patched one.\\nMake sure you have backed up personal data.</string>\n    <string name=\"patch_install_successfully\">Install successfully</string>\n    <string name=\"patch_install_failed\">Install failed</string>\n    <string name=\"patch_no_xposed_module\">No Xposed module(s) were found</string>\n\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Select Apps</string>\n\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Settings</string>\n    <string name=\"settings_keystore\">Signature keystore</string>\n    <string name=\"settings_keystore_default\">Built-in</string>\n    <string name=\"settings_keystore_custom\">Custom</string>\n    <string name=\"settings_keystore_dialog_title\">Custom keystore</string>\n    <string name=\"settings_keystore_file\">Keystore file</string>\n    <string name=\"settings_keystore_password\">Password</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias password</string>\n    <string name=\"settings_keystore_desc\">Set keystore (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Wrong type of keystore</string>\n    <string name=\"settings_keystore_wrong_password\">Wrong keystore password</string>\n    <string name=\"settings_keystore_wrong_alias\">Wrong alias name</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Wrong alias password</string>\n    <string name=\"settings_detail_patch_logs\">Detail patch logs</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values/strings_untranslatable.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">LSPatch</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-af/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Voeg by</string>\n    <string name=\"install\">Installeer</string>\n    <string name=\"installing\">Installeer tans</string>\n    <string name=\"uninstall\">Deïnstalleer</string>\n    <string name=\"uninstalling\">Deïnstalleer tans</string>\n    <string name=\"copy_error\">Copy error</string>\n    <string name=\"apps\">برنامه ها</string>\n    <string name=\"modules\">Modules</string>\n    <string name=\"shizuku_available\">Shizuku diens beskikbaar</string>\n    <string name=\"shizuku_unavailable\">Shizuku-diens nie gekoppel nie</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Logs</string>\n    <string name=\"off\">Af</string>\n    <string name=\"error_unknown\">Onbekende fout</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Sommige funksies nie beskikbaar nie</string>\n    <string name=\"home_api_version\">API weergawe</string>\n    <string name=\"home_lspatch_version\">LSPatch weergawe</string>\n    <string name=\"home_framework_version\">Raamwerk weergawe</string>\n    <string name=\"home_system_version\">Stelsel weergawe</string>\n    <string name=\"home_device\">Toestel</string>\n    <string name=\"home_system_abi\">Stelsel ABI</string>\n    <string name=\"home_info_copied\">Gekopieer na knipbord</string>\n    <string name=\"home_support\">Ondersteuning</string>\n    <string name=\"home_description\">LSPatch is \\'n gratis nie-wortel Xposed-raamwerk gebaseer op LSPosed-kern.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Bekyk bronkode by %1$s<br/>Sluit aan by ons %2$s -kanaal]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Bestuur</string>\n    <string name=\"manage_loading\">Laai tans</string>\n    <string name=\"manage_no_apps\">Nog geen reggemaakte programme nie</string>\n    <string name=\"manage_rolling\">Rol</string>\n    <string name=\"manage_update_loader\">Dateer laaier op</string>\n    <string name=\"manage_update_loader_successfully\">Dateer suksesvol op</string>\n    <string name=\"manage_update_loader_failed\">Opdatering misluk</string>\n    <string name=\"manage_module_scope\">Module omvang</string>\n    <string name=\"manage_optimize\">Optimaliseer</string>\n    <string name=\"manage_optimize_successfully\">Optimaliseer suksesvol</string>\n    <string name=\"manage_optimize_failed\">Kon nie optimaliseer nie</string>\n    <string name=\"manage_uninstall_successfully\">Deïnstalleer suksesvol</string>\n    <string name=\"manage_no_modules\">Nog geen modules nie</string>\n    <string name=\"manage_module_settings\">Module instellings</string>\n    <string name=\"manage_app_info\">App inligting</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nuwe Patch</string>\n    <string name=\"patch_select_dir_title\">Kies bergingsgids</string>\n    <string name=\"patch_select_dir_text\">Kies \\'n gids om die gelapte APK te stoor</string>\n    <string name=\"patch_select_dir_error\">Fout met die opstel van berginggids</string>\n    <string name=\"patch_from_storage\">Kies APK(s) uit die stoor</string>\n    <string name=\"patch_from_applist\">Kies \\'n geïnstalleerde toepassing</string>\n    <string name=\"patch_mode\">Patch Mode</string>\n    <string name=\"patch_local\">Plaaslik</string>\n    <string name=\"patch_local_desc\">Maak \\'n toepassing sonder modules ingebed.\\nXposed omvang kan dinamies verander word sonder her-patch.\\nPlaaslike gelapte programme kan slegs op die plaaslike toestel loop.</string>\n    <string name=\"patch_integrated\">Geïntegreerde</string>\n    <string name=\"patch_integrated_desc\">Patcher une application avec des modules intégrés.\\nL\\'application corrigée peut fonctionner sans le gestionnaire mais ne peut pas être gérée dynamiquement.\\nLes applications patchées intégrées peuvent être utilisées sur des appareils sur lesquels LSPatch Manager n\\'est pas installé.</string>\n    <string name=\"patch_embed_modules\">Sluit modules in</string>\n    <string name=\"patch_debuggable\">Ontfoutbaar</string>\n    <string name=\"patch_sigbypass\">Handtekening omseil</string>\n    <string name=\"patch_sigbypasslv0\">lv0: af</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Omseil PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Omseil PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Ignoreer weergawekode</string>\n    <string name=\"patch_override_version_code_desc\">Ignoreer die gelapte toepassing se weergawekode na 1\\nDit laat afgradering installasie in die toekoms toe, en oor die algemeen sal dit nie die weergawekode beïnvloed wat werklik deur die toepassing waargeneem word nie</string>\n    <string name=\"patch_start\">Begin Patch</string>\n    <string name=\"patch_return\">Keer terug</string>\n    <string name=\"patch_uninstall_text\">As gevolg van verskillende handtekeninge, moet u die oorspronklike toepassing deïnstalleer voordat u die gelapte een installeer.\\nMaak seker dat jy persoonlike data gerugsteun het.</string>\n    <string name=\"patch_install_successfully\">Installeer suksesvol</string>\n    <string name=\"patch_install_failed\">Installering het misluk</string>\n    <string name=\"patch_no_xposed_module\">Geen Xposed-module(s) is gevind nie</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Kies Toepassings</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Instellings</string>\n    <string name=\"settings_keystore\">Armazenamento de assinatura</string>\n    <string name=\"settings_keystore_default\">Ingeboude</string>\n    <string name=\"settings_keystore_custom\">Pasgemaak</string>\n    <string name=\"settings_keystore_dialog_title\">Pasgemaakte sleutelstoor</string>\n    <string name=\"settings_keystore_file\">Sleutelstoor lêer</string>\n    <string name=\"settings_keystore_password\">Wagwoord</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias wagwoord</string>\n    <string name=\"settings_keystore_desc\">Stel sleutelstoor (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Verkeerde tipe sleutelstoor</string>\n    <string name=\"settings_keystore_wrong_password\">Verkeerde sleutelstoor wagwoord</string>\n    <string name=\"settings_keystore_wrong_alias\">Verkeerde alias naam</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Verkeerde alias wagwoord</string>\n    <string name=\"settings_detail_patch_logs\">Detail pleister logs</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">إضافة</string>\n    <string name=\"install\">تثبيت</string>\n    <string name=\"installing\">جارٍ التثبيت</string>\n    <string name=\"uninstall\">إلغاء التثبيت</string>\n    <string name=\"uninstalling\">جارٍ إلغاء التثبيت</string>\n    <string name=\"copy_error\">خطأ في النسخ</string>\n    <string name=\"apps\">التطبيقات</string>\n    <string name=\"modules\">الوحدات</string>\n    <string name=\"shizuku_available\">خدمة Shizuku متاحة</string>\n    <string name=\"shizuku_unavailable\">خدمة Shizuku غير متصلة</string>\n    <string name=\"screen_repo\">المستودع</string>\n    <string name=\"screen_logs\">السجلات</string>\n    <string name=\"off\">متوقف</string>\n    <string name=\"error_unknown\">Unknown error</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">بعض الوظائف غير متاحة</string>\n    <string name=\"home_api_version\">إصدار API</string>\n    <string name=\"home_lspatch_version\">إصدار LSPatch</string>\n    <string name=\"home_framework_version\">إصدار Framework</string>\n    <string name=\"home_system_version\">إصدار النظام</string>\n    <string name=\"home_device\">الجهاز</string>\n    <string name=\"home_system_abi\">نظام ABI</string>\n    <string name=\"home_info_copied\">تم النسخ إلى الحافظة</string>\n    <string name=\"home_support\">الدعـم</string>\n    <string name=\"home_description\">LSPatch هو إطار عمل Non-Root Xposed مجاني يعتمد على LSPosed core.</string>\n    <string name=\"home_view_source_code\"><![CDATA[عرض كود المصدر على %1$s<br/>انضم إلى قناتنا %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">إدارة</string>\n    <string name=\"manage_loading\">جارٍ التحميل</string>\n    <string name=\"manage_no_apps\">لا توجد تطبيقات معدلة حتى الآن</string>\n    <string name=\"manage_rolling\">المتداول</string>\n    <string name=\"manage_update_loader\">تحديث المحمل</string>\n    <string name=\"manage_update_loader_successfully\">تم التحديث بنجاح</string>\n    <string name=\"manage_update_loader_failed\">فشل التحديث</string>\n    <string name=\"manage_module_scope\">نطاق الوحدة</string>\n    <string name=\"manage_optimize\">تحسين</string>\n    <string name=\"manage_optimize_successfully\">تم التحسين بنجاح</string>\n    <string name=\"manage_optimize_failed\">فشل التحسين</string>\n    <string name=\"manage_uninstall_successfully\">تم إلغاء التثبيت بنجاح</string>\n    <string name=\"manage_no_modules\">لا توجد وحدات بعد</string>\n    <string name=\"manage_module_settings\">إعدادات الوحدة</string>\n    <string name=\"manage_app_info\">معلومات التطبيق</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">تعديل جديد</string>\n    <string name=\"patch_select_dir_title\">حدد مجلد التخزين</string>\n    <string name=\"patch_select_dir_text\">حدد مجلد لتخزين التطبيقات المعدلة</string>\n    <string name=\"patch_select_dir_error\">خطأ عند تعيين مجلد التخزين</string>\n    <string name=\"patch_from_storage\">تحديد APK(s) من التخزين</string>\n    <string name=\"patch_from_applist\">تحديد تطبيقًا مثبتًا</string>\n    <string name=\"patch_mode\">وضع التعديل</string>\n    <string name=\"patch_local\">محلي</string>\n    <string name=\"patch_local_desc\">قم بتصحيح تطبيق بدون وحدات مدمجة.\\nيمكن تغيير نطاق Xpose ديناميكيًا دون إعادة التصحيح.\\nلا يمكن تشغيل التطبيقات المحلية المصححة إلا على الجهاز المحلي.</string>\n    <string name=\"patch_integrated\">متكامل</string>\n    <string name=\"patch_integrated_desc\">تعديل تطبيق بوحدات مدمجة.\\nيمكن تشغيل التطبيق المعدل بدون المدير، لكن لا يمكن إدارته ديناميكيًا.\\nيمكن استخدام التطبيقات المعدلة المدمجة على الأجهزة التي لم يتم تثبيت LSPatch Manager عليها.</string>\n    <string name=\"patch_embed_modules\">الوحدات المُضْمَنة</string>\n    <string name=\"patch_debuggable\">قابل للتصحيح</string>\n    <string name=\"patch_sigbypass\">تجاوز التوقيع</string>\n    <string name=\"patch_sigbypasslv0\">مستوى 0: مغلق</string>\n    <string name=\"patch_sigbypasslv1\">مستوى 1: تجاوز PM</string>\n    <string name=\"patch_sigbypasslv2\">مستوى 2: تجاوز PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">تجاوز رمز الإصدار</string>\n    <string name=\"patch_override_version_code_desc\">تجاوز رمز إصدار التطبيق المعدل إلى 1\\nوهذا يسمح بالرجوع إلى إصدار أقدم في المستقبل، وعمومًا لن يؤثر هذا على رمز الإصدار الذي يتصوره التطبيق بالفعل</string>\n    <string name=\"patch_start\">بدء التعديل</string>\n    <string name=\"patch_return\">رجوع</string>\n    <string name=\"patch_uninstall_text\">نظرًا لاختلاف التوقيعات، يلزمك إلغاء تثبيت التطبيق الأصلي قبل تثبيت التطبيق المعدل.\\nتأكد من عمل نسخة احتياطية من البيانات الشخصية.</string>\n    <string name=\"patch_install_successfully\">تم التثبيت بنجاح</string>\n    <string name=\"patch_install_failed\">فشل التثبيت</string>\n    <string name=\"patch_no_xposed_module\"></string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">تحديد التطبيقات</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">الإعدادات</string>\n    <string name=\"settings_keystore\">مخزن مفاتيح التوقيع</string>\n    <string name=\"settings_keystore_default\">مدمج</string>\n    <string name=\"settings_keystore_custom\">مخصص</string>\n    <string name=\"settings_keystore_dialog_title\">تخزين المفاتيح المخصص</string>\n    <string name=\"settings_keystore_file\">ملف Keystore</string>\n    <string name=\"settings_keystore_password\">كلمه السر</string>\n    <string name=\"settings_keystore_alias\">الاسم المستعار</string>\n    <string name=\"settings_keystore_alias_password\">كلمة مرور الاسم المستعار</string>\n    <string name=\"settings_keystore_desc\">تعيين مخزن المفاتيح (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">نوع خاطئ من ملف تخزين المفاتيح</string>\n    <string name=\"settings_keystore_wrong_password\">كلمة مرور تخزين المفاتيح خاطئة</string>\n    <string name=\"settings_keystore_wrong_alias\">اسم مستعار خاطئ</string>\n    <string name=\"settings_keystore_wrong_alias_password\">كلمة مرور الاسم المستعار خاطئة</string>\n    <string name=\"settings_detail_patch_logs\">تفاصيل سجلات التعديل</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-bg/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Добавяне на</string>\n    <string name=\"install\">Инсталиране на</string>\n    <string name=\"installing\">Инсталиране на</string>\n    <string name=\"uninstall\">Деинсталиране на</string>\n    <string name=\"uninstalling\">Деинсталиране на</string>\n    <string name=\"copy_error\">Грешка при копиране</string>\n    <string name=\"apps\">Приложения</string>\n    <string name=\"modules\">Модули</string>\n    <string name=\"shizuku_available\">Налична услуга Shizuku</string>\n    <string name=\"shizuku_unavailable\">Услугата Shizuku не е свързана</string>\n    <string name=\"screen_repo\">Репо</string>\n    <string name=\"screen_logs\">Дневници</string>\n    <string name=\"off\">Изключено</string>\n    <string name=\"error_unknown\">Неизвестна грешка</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Някои функции са недостъпни</string>\n    <string name=\"home_api_version\">Версия на API</string>\n    <string name=\"home_lspatch_version\">Версия на LSPatch</string>\n    <string name=\"home_framework_version\">Версия на рамката</string>\n    <string name=\"home_system_version\">Версия на системата</string>\n    <string name=\"home_device\">Устройство</string>\n    <string name=\"home_system_abi\">ABI на системата</string>\n    <string name=\"home_info_copied\">Копиране в клипборда</string>\n    <string name=\"home_support\">Подкрепа</string>\n    <string name=\"home_description\">LSPatch е безплатна некоренова рамка Xposed, базирана на ядрото LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Преглед на изходния код в %1$s<br/>Присъединете се към нашия %2$s канал]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Управление на</string>\n    <string name=\"manage_loading\">Зареждане на</string>\n    <string name=\"manage_no_apps\">Все още няма поправени приложения</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Актуализиране на товарача</string>\n    <string name=\"manage_update_loader_successfully\">Успешно актуализиране</string>\n    <string name=\"manage_update_loader_failed\">Обновяването е неуспешно</string>\n    <string name=\"manage_module_scope\">Обхват на модула</string>\n    <string name=\"manage_optimize\">Оптимизиране на</string>\n    <string name=\"manage_optimize_successfully\">Оптимизирайте успешно</string>\n    <string name=\"manage_optimize_failed\">Оптимизиране на неуспешни</string>\n    <string name=\"manage_uninstall_successfully\">Успешно деинсталиране</string>\n    <string name=\"manage_no_modules\">Все още няма модули</string>\n    <string name=\"manage_module_settings\">Настройки на модула</string>\n    <string name=\"manage_app_info\">Информация за приложението</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Нова кръпка</string>\n    <string name=\"patch_select_dir_title\">Изберете директория за съхранение</string>\n    <string name=\"patch_select_dir_text\">Изберете директория, в която да съхранявате поправените apks</string>\n    <string name=\"patch_select_dir_error\">Грешка при задаване на директория за съхранение</string>\n    <string name=\"patch_from_storage\">Изберете apk(s) от хранилището</string>\n    <string name=\"patch_from_applist\">Изберете инсталирано приложение</string>\n    <string name=\"patch_mode\">Режим Patch</string>\n    <string name=\"patch_local\">Местни</string>\n    <string name=\"patch_local_desc\">Поправка на приложение без вградени модули.\\nОбхватът на Xposed може да бъде променян динамично, без да се налага повторна кръпка.\\nЛокално патчираните приложения могат да работят само на локалното устройство.</string>\n    <string name=\"patch_integrated\">Интегриран</string>\n    <string name=\"patch_integrated_desc\">Пач на приложение с вградени модули.\\nПриложението с корекция може да работи без мениджъра, но не може да се управлява динамично.\\nИнтегрираните пакетирани приложения могат да се използват на устройства, които нямат инсталиран LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Вграждане на модули</string>\n    <string name=\"patch_debuggable\">Отстраняване на грешки</string>\n    <string name=\"patch_sigbypass\">Подписване на байпас</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Изключено</string>\n    <string name=\"patch_sigbypasslv1\">1 лев: Заобикаляне на PM</string>\n    <string name=\"patch_sigbypasslv2\">2 лв: Заобикаляне на PM openat (libc)</string>\n    <string name=\"patch_override_version_code\">Код на версията за пренастройване</string>\n    <string name=\"patch_override_version_code_desc\">Презаписване на кода на версията на коригираното приложение на 1\\nТова позволява понижаване на инсталацията в бъдеще и като цяло няма да се отрази на кода на версията, възприеман от приложението.</string>\n    <string name=\"patch_start\">Начална кръпка</string>\n    <string name=\"patch_return\">Връщане на</string>\n    <string name=\"patch_uninstall_text\">Поради различните сигнатури е необходимо да деинсталирате оригиналното приложение, преди да инсталирате коригираното.\\nУверете се, че сте направили резервно копие на личните си данни.</string>\n    <string name=\"patch_install_successfully\">Инсталирайте успешно</string>\n    <string name=\"patch_install_failed\">Инсталацията е неуспешна</string>\n    <string name=\"patch_no_xposed_module\">Не са открити модул(и) Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Изберете приложения</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Настройки</string>\n    <string name=\"settings_keystore\">Хранилище на ключове за подписване</string>\n    <string name=\"settings_keystore_default\">Вграден</string>\n    <string name=\"settings_keystore_custom\">Потребителски</string>\n    <string name=\"settings_keystore_dialog_title\">Потребителско хранилище на ключове</string>\n    <string name=\"settings_keystore_file\">Файл на хранилището за ключове</string>\n    <string name=\"settings_keystore_password\">Парола</string>\n    <string name=\"settings_keystore_alias\">Псевдоним</string>\n    <string name=\"settings_keystore_alias_password\">Псевдоним за парола</string>\n    <string name=\"settings_keystore_desc\">Задаване на хранилище на ключове (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Неправилен тип хранилище на ключове</string>\n    <string name=\"settings_keystore_wrong_password\">Грешна парола на хранилището на ключове</string>\n    <string name=\"settings_keystore_wrong_alias\">Неправилно име на псевдоним</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Грешна парола за псевдоним</string>\n    <string name=\"settings_detail_patch_logs\">Подробни дневници на кръпките</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-bn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">যোগ করুন</string>\n    <string name=\"install\">ইনস্টল করুন</string>\n    <string name=\"installing\">ইনস্টল করা হচ্ছে</string>\n    <string name=\"uninstall\">আনইনস্টল করুন</string>\n    <string name=\"uninstalling\">আনইনস্টল হচ্ছে</string>\n    <string name=\"copy_error\">কপি ত্রুটি</string>\n    <string name=\"apps\">অ্যাপস</string>\n    <string name=\"modules\">মডিউল</string>\n    <string name=\"shizuku_available\">শিজুকু পরিষেবা উপলব্ধ</string>\n    <string name=\"shizuku_unavailable\">Shizuku পরিষেবা সংযুক্ত নেই৷</string>\n    <string name=\"screen_repo\">রেপো</string>\n    <string name=\"screen_logs\">লগ</string>\n    <string name=\"off\">বন্ধ</string>\n    <string name=\"error_unknown\">অজানা ত্রুটি</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">কিছু ফাংশন অনুপলব্ধ</string>\n    <string name=\"home_api_version\">API সংস্করণ</string>\n    <string name=\"home_lspatch_version\">এলএসপ্যাচ সংস্করণ</string>\n    <string name=\"home_framework_version\">ফ্রেমওয়ার্ক সংস্করণ</string>\n    <string name=\"home_system_version\">সিস্টেম সংস্করণ</string>\n    <string name=\"home_device\">যন্ত্র</string>\n    <string name=\"home_system_abi\">সিস্টেম ABI</string>\n    <string name=\"home_info_copied\">ক্লিপবোর্ডে কপি করা হয়েছে</string>\n    <string name=\"home_support\">সমর্থন</string>\n    <string name=\"home_description\">LSPatch হল LSPosed কোরের উপর ভিত্তি করে একটি বিনামূল্যের নন-রুট Xposed ফ্রেমওয়ার্ক।</string>\n    <string name=\"home_view_source_code\"><![CDATA[%1$s<br/>এ সোর্স কোড দেখুন আমাদের %2$s চ্যানেলে যোগ দিন]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">পরিচালনা করুন</string>\n    <string name=\"manage_loading\">লোড হচ্ছে</string>\n    <string name=\"manage_no_apps\">এখনো কোনো প্যাচড অ্যাপস</string>\n    <string name=\"manage_rolling\">ঘূর্ণায়মান</string>\n    <string name=\"manage_update_loader\">লোডার আপডেট করুন</string>\n    <string name=\"manage_update_loader_successfully\">সফলভাবে আপডেট</string>\n    <string name=\"manage_update_loader_failed\">হালনাগাদ ব্যর্থ হয়েছে</string>\n    <string name=\"manage_module_scope\">মডিউল সুযোগ</string>\n    <string name=\"manage_optimize\">অপ্টিমাইজ করুন</string>\n    <string name=\"manage_optimize_successfully\">সফলভাবে অপ্টিমাইজ করুন</string>\n    <string name=\"manage_optimize_failed\">অপ্টিমাইজ ব্যর্থ হয়েছে৷</string>\n    <string name=\"manage_uninstall_successfully\">সফলভাবে আনইনস্টল</string>\n    <string name=\"manage_no_modules\">এখনো কোনো মডিউল নেই</string>\n    <string name=\"manage_module_settings\">মডিউল সেটিংস</string>\n    <string name=\"manage_app_info\">অ্যাপের তথ্য</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">নতুন প্যাচ</string>\n    <string name=\"patch_select_dir_title\">স্টোরেজ ডিরেক্টরি নির্বাচন করুন</string>\n    <string name=\"patch_select_dir_text\">প্যাচ করা apks সংরক্ষণ করতে একটি ডিরেক্টরি নির্বাচন করুন</string>\n    <string name=\"patch_select_dir_error\">স্টোরেজ ডিরেক্টরি সেট করার সময় ত্রুটি</string>\n    <string name=\"patch_from_storage\">স্টোরেজ থেকে apk(গুলি) নির্বাচন করুন</string>\n    <string name=\"patch_from_applist\">একটি ইনস্টল করা অ্যাপ্লিকেশন নির্বাচন করুন</string>\n    <string name=\"patch_mode\">প্যাচ মোড</string>\n    <string name=\"patch_local\">স্থানীয়</string>\n    <string name=\"patch_local_desc\">এম্বেড করা মডিউল ছাড়াই একটি অ্যাপ প্যাচ করুন।\\nএক্সপোজড স্কোপ রি-প্যাচ ছাড়াই গতিশীলভাবে পরিবর্তন করা যেতে পারে।\\nস্থানীয় প্যাচ করা অ্যাপ শুধুমাত্র স্থানীয় ডিভাইসে চলতে পারে।</string>\n    <string name=\"patch_integrated\">কই তুমি</string>\n    <string name=\"patch_integrated_desc\">কি নাই.</string>\n    <string name=\"patch_embed_modules\">করছি</string>\n    <string name=\"patch_debuggable\">ডিবাগযোগ্য</string>\n    <string name=\"patch_sigbypass\">স্বাক্ষর বাইপাস</string>\n    <string name=\"patch_sigbypasslv0\">lv0: বন্ধ</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM বাইপাস করুন</string>\n    <string name=\"patch_sigbypasslv2\">lv2: বাইপাস PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">সংস্করণ কোড ওভাররাইড করুন</string>\n    <string name=\"patch_override_version_code_desc\">প্যাচ করা অ্যাপের সংস্করণ কোডটিকে 1\\nএ ওভাররাইড করুন এটি ভবিষ্যতে ইনস্টলেশন ডাউনগ্রেড করার অনুমতি দেয় এবং সাধারণত এটি অ্যাপ্লিকেশন দ্বারা অনুভূত সংস্করণ কোডকে প্রভাবিত করবে না</string>\n    <string name=\"patch_start\">প্যাচ শুরু করুন</string>\n    <string name=\"patch_return\">প্রত্যাবর্তন</string>\n    <string name=\"patch_uninstall_text\">বিভিন্ন স্বাক্ষরের কারণে, প্যাচ করা একটি ইনস্টল করার আগে আপনাকে আসল অ্যাপটি আনইনস্টল করতে হবে।\\nআপনি ব্যক্তিগত ডেটা ব্যাক আপ করেছেন তা নিশ্চিত করুন৷</string>\n    <string name=\"patch_install_successfully\">সফলভাবে ইনস্টল করুন</string>\n    <string name=\"patch_install_failed\">ইনস্টল ব্যর্থ হয়েছে</string>\n    <string name=\"patch_no_xposed_module\">No Xposed module(s) were found</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">অ্যাপস নির্বাচন করুন</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">সেটিংস</string>\n    <string name=\"settings_keystore\">স্বাক্ষর কীস্টোর</string>\n    <string name=\"settings_keystore_default\">অন্তর্নির্মিত</string>\n    <string name=\"settings_keystore_custom\">কাস্টম</string>\n    <string name=\"settings_keystore_dialog_title\">কাস্টম কীস্টোর</string>\n    <string name=\"settings_keystore_file\">কীস্টোর ফাইল</string>\n    <string name=\"settings_keystore_password\">পাসওয়ার্ড</string>\n    <string name=\"settings_keystore_alias\">উপনাম</string>\n    <string name=\"settings_keystore_alias_password\">উপনাম পাসওয়ার্ড</string>\n    <string name=\"settings_keystore_desc\">সেট কীস্টোর (বিকেএস)</string>\n    <string name=\"settings_keystore_wrong_keystore\">কীস্টোরের ভুল ধরন</string>\n    <string name=\"settings_keystore_wrong_password\">ভুল কীস্টোর পাসওয়ার্ড</string>\n    <string name=\"settings_keystore_wrong_alias\">ভুল উপনাম নাম</string>\n    <string name=\"settings_keystore_wrong_alias_password\">ভুল ওরফে পাসওয়ার্ড</string>\n    <string name=\"settings_detail_patch_logs\">বিস্তারিত প্যাচ লগ</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ca/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Afegeix</string>\n    <string name=\"install\">Instal·lar</string>\n    <string name=\"installing\">Instal·lació</string>\n    <string name=\"uninstall\">Desinstal·la</string>\n    <string name=\"uninstalling\">S\\'està desinstal·lant</string>\n    <string name=\"copy_error\">Error de còpia</string>\n    <string name=\"apps\">Aplicacions</string>\n    <string name=\"modules\">Mòduls</string>\n    <string name=\"shizuku_available\">Servei Shizuku disponible</string>\n    <string name=\"shizuku_unavailable\">El servei Shizuku no està connectat</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Registres</string>\n    <string name=\"off\">Apagat</string>\n    <string name=\"error_unknown\">Error desconegut</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Algunes funcions no estan disponibles</string>\n    <string name=\"home_api_version\">Versió de l\\'API</string>\n    <string name=\"home_lspatch_version\">Versió LSPatch</string>\n    <string name=\"home_framework_version\">Versió del marc</string>\n    <string name=\"home_system_version\">Versió del sistema</string>\n    <string name=\"home_device\">Dispositiu</string>\n    <string name=\"home_system_abi\">Sistema ABI</string>\n    <string name=\"home_info_copied\">S\\'ha copiat al porta-retalls</string>\n    <string name=\"home_support\">Suport</string>\n    <string name=\"home_description\">LSPatch és un marc Xposed gratuït no root basat en el nucli LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Veure el codi font a %1$s<br/>Uneix-te al nostre canal %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gestionar</string>\n    <string name=\"manage_loading\">Carregant</string>\n    <string name=\"manage_no_apps\">Encara no hi ha cap aplicació pegada</string>\n    <string name=\"manage_rolling\">Rodant</string>\n    <string name=\"manage_update_loader\">Actualitza el carregador</string>\n    <string name=\"manage_update_loader_successfully\">Actualitza correctament</string>\n    <string name=\"manage_update_loader_failed\">No s\\'ha pogut actualitzar</string>\n    <string name=\"manage_module_scope\">Àmbit del mòdul</string>\n    <string name=\"manage_optimize\">Optimitzar</string>\n    <string name=\"manage_optimize_successfully\">Optimitzar amb èxit</string>\n    <string name=\"manage_optimize_failed\">L\\'optimització ha fallat</string>\n    <string name=\"manage_uninstall_successfully\">Desinstal·la correctament</string>\n    <string name=\"manage_no_modules\">Encara no hi ha mòduls</string>\n    <string name=\"manage_module_settings\">Configuració del mòdul</string>\n    <string name=\"manage_app_info\">Informació de l\\'aplicació</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nou Pegat</string>\n    <string name=\"patch_select_dir_title\">Seleccioneu el directori d\\'emmagatzematge</string>\n    <string name=\"patch_select_dir_text\">Seleccioneu un directori per emmagatzemar els apks pegats</string>\n    <string name=\"patch_select_dir_error\">Error en configurar el directori d\\'emmagatzematge</string>\n    <string name=\"patch_from_storage\">Seleccioneu apk(s) de l\\'emmagatzematge</string>\n    <string name=\"patch_from_applist\">Seleccioneu una aplicació instal·lada</string>\n    <string name=\"patch_mode\">Mode de pegat</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Apliqueu una aplicació sense mòduls incrustats.\\nL\\'àmbit Xposed es pot canviar dinàmicament sense tornar a aplicar el pedaç.\\nLes aplicacions localitzades només es poden executar al dispositiu local.</string>\n    <string name=\"patch_integrated\">Integrat</string>\n    <string name=\"patch_integrated_desc\">Apliqueu una aplicació amb mòduls incrustats.\\nL\\'aplicació pegada es pot executar sense el gestor, però no es pot gestionar de manera dinàmica.\\nLes aplicacions amb pedaços integrades es poden utilitzar en dispositius que no tinguin LSPatch Manager instal·lat.</string>\n    <string name=\"patch_embed_modules\">Incrustar mòduls</string>\n    <string name=\"patch_debuggable\">Depurable</string>\n    <string name=\"patch_sigbypass\">Bypass de signatura</string>\n    <string name=\"patch_sigbypasslv0\">lv0: apagat</string>\n    <string name=\"patch_sigbypasslv1\">lv1: anul·lar PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: ometre PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Anul·la el codi de versió</string>\n    <string name=\"patch_override_version_code_desc\">Substituïu el codi de versió de l\\'aplicació aplicada a 1\\nAixò permetrà la instal·lació de baixada en el futur i, en general, això no afectarà el codi de versió realment percebut per l\\'aplicació.</string>\n    <string name=\"patch_start\">Inicia el pegat</string>\n    <string name=\"patch_return\">Tornar</string>\n    <string name=\"patch_uninstall_text\">A causa de les diferents signatures, heu de desinstal·lar l\\'aplicació original abans d\\'instal·lar la pegada.\\nAssegureu-vos que heu fet una còpia de seguretat de les dades personals.</string>\n    <string name=\"patch_install_successfully\">Instal·la correctament</string>\n    <string name=\"patch_install_failed\">La instal·lació ha fallat</string>\n    <string name=\"patch_no_xposed_module\">No s\\'han trobat mòduls Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Seleccioneu Aplicacions</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Configuració</string>\n    <string name=\"settings_keystore\">Botiga de claus de signatures</string>\n    <string name=\"settings_keystore_default\">Integrat</string>\n    <string name=\"settings_keystore_custom\">Personalitzat</string>\n    <string name=\"settings_keystore_dialog_title\">Magatzem de claus personalitzat</string>\n    <string name=\"settings_keystore_file\">Fitxer de magatzem de claus</string>\n    <string name=\"settings_keystore_password\">Contrasenya</string>\n    <string name=\"settings_keystore_alias\">Àlies</string>\n    <string name=\"settings_keystore_alias_password\">Contrasenya d\\'àlies</string>\n    <string name=\"settings_keystore_desc\">Estableix el magatzem de claus (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Tipus de magatzem de claus incorrecte</string>\n    <string name=\"settings_keystore_wrong_password\">La contrasenya del magatzem de claus és incorrecta</string>\n    <string name=\"settings_keystore_wrong_alias\">Nom d\\'àlies incorrecte</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Contrasenya d\\'àlies incorrecta</string>\n    <string name=\"settings_detail_patch_logs\">Registres de pedaços detallats</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Přidat</string>\n    <string name=\"install\">Instalace</string>\n    <string name=\"installing\">Instalace stránek</string>\n    <string name=\"uninstall\">Odinstalujte stránku</string>\n    <string name=\"uninstalling\">Odinstalování stránky</string>\n    <string name=\"copy_error\">Chyba kopírování</string>\n    <string name=\"apps\">Aplikace</string>\n    <string name=\"modules\">Moduly</string>\n    <string name=\"shizuku_available\">K dispozici služba Shizuku</string>\n    <string name=\"shizuku_unavailable\">Služba Shizuku není připojena</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Protokoly</string>\n    <string name=\"off\">Vypnuto</string>\n    <string name=\"error_unknown\">Neznámá chyba</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Některé funkce nejsou k dispozici</string>\n    <string name=\"home_api_version\">Verze API</string>\n    <string name=\"home_lspatch_version\">Verze LSPatch</string>\n    <string name=\"home_framework_version\">Verze rámce</string>\n    <string name=\"home_system_version\">Verze systému</string>\n    <string name=\"home_device\">přístroj</string>\n    <string name=\"home_system_abi\">Systém ABI</string>\n    <string name=\"home_info_copied\">Zkopírováno do schránky</string>\n    <string name=\"home_support\">Podpěra, podpora</string>\n    <string name=\"home_description\">LSPatch je bezplatný non-root Xposed framework založený na jádru LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Zobrazit zdrojový kód na %1$s<br/>Připojte se k našemu %2$s kanálu]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Správa</string>\n    <string name=\"manage_loading\">načítání</string>\n    <string name=\"manage_no_apps\">Zatím žádné opravené aplikace</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Aktualizace zavaděče</string>\n    <string name=\"manage_update_loader_successfully\">Úspěšná aktualizace</string>\n    <string name=\"manage_update_loader_failed\">Aktualizace se nezdařila</string>\n    <string name=\"manage_module_scope\">Rozsah modulu</string>\n    <string name=\"manage_optimize\">Optimalizace</string>\n    <string name=\"manage_optimize_successfully\">Úspěšná optimalizace</string>\n    <string name=\"manage_optimize_failed\">Optimalizace selhala</string>\n    <string name=\"manage_uninstall_successfully\">Úspěšná odinstalace</string>\n    <string name=\"manage_no_modules\">Zatím žádné moduly</string>\n    <string name=\"manage_module_settings\">Nastavení modulu</string>\n    <string name=\"manage_app_info\">Informace o aplikaci</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nová záplata</string>\n    <string name=\"patch_select_dir_title\">Vyberte adresář úložiště</string>\n    <string name=\"patch_select_dir_text\">Vyberte adresář pro uložení opravených souborů APK</string>\n    <string name=\"patch_select_dir_error\">Chyba při nastavování adresáře úložiště</string>\n    <string name=\"patch_from_storage\">Vyberte apk z úložiště</string>\n    <string name=\"patch_from_applist\">Vyberte nainstalovanou aplikaci</string>\n    <string name=\"patch_mode\">Režim opravy</string>\n    <string name=\"patch_local\">Místní</string>\n    <string name=\"patch_local_desc\">Oprava aplikace bez vložených modulů.\\nRozsah Xposed lze dynamicky měnit bez opětovného záplatování.\\nMístní záplatované aplikace lze spustit pouze na místním zařízení.</string>\n    <string name=\"patch_integrated\">Integrovaný</string>\n    <string name=\"patch_integrated_desc\">Opravte aplikaci s vestavěnými moduly.\\nOpravená aplikace může běžet bez správce, ale nelze ji spravovat dynamicky.\\nIntegrované opravené aplikace lze používat na zařízeních, která nemají nainstalovaný LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Vložit moduly</string>\n    <string name=\"patch_debuggable\">Laditelné</string>\n    <string name=\"patch_sigbypass\">Přemostění podpisu</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Vypnuto</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Obejít PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Přepsat kód verze</string>\n    <string name=\"patch_override_version_code_desc\">Přepsat kód verze opravené aplikace na 1\\nTo umožní v budoucnu downgradovat (snížit verzi) a obecně to neovlivní kód verze, který aplikace skutečně vnímá</string>\n    <string name=\"patch_start\">Spusťte opravu</string>\n    <string name=\"patch_return\">Vrátit se</string>\n    <string name=\"patch_uninstall_text\">Kvůli různým podpisům musíte před instalací opravené odinstalovat původní aplikaci.\\nUjistěte se, že máte zálohovaná osobní data.</string>\n    <string name=\"patch_install_successfully\">Nainstalujte úspěšně</string>\n    <string name=\"patch_install_failed\">Instalace se nezdařila</string>\n    <string name=\"patch_no_xposed_module\">Nebyly nalezeny žádné moduly Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Vybrané aplikace</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Nastavení</string>\n    <string name=\"settings_keystore\">Úložiště klíčů s podpisem</string>\n    <string name=\"settings_keystore_default\">Vestavěný</string>\n    <string name=\"settings_keystore_custom\">Vlastní</string>\n    <string name=\"settings_keystore_dialog_title\">Vlastní úložiště klíčů</string>\n    <string name=\"settings_keystore_file\">Soubor úložiště klíčů</string>\n    <string name=\"settings_keystore_password\">Heslo</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias heslo</string>\n    <string name=\"settings_keystore_desc\">Nastavení úložiště klíčů (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Nesprávný typ úložiště klíčů</string>\n    <string name=\"settings_keystore_wrong_password\">Špatné heslo úložiště klíčů</string>\n    <string name=\"settings_keystore_wrong_alias\">Špatný název aliasu</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Špatné heslo aliasu</string>\n    <string name=\"settings_detail_patch_logs\">Podrobné protokoly oprav</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-da/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Tilføje</string>\n    <string name=\"install\">Installer</string>\n    <string name=\"installing\">Installation af</string>\n    <string name=\"uninstall\">Afinstaller</string>\n    <string name=\"uninstalling\">Afinstallation af</string>\n    <string name=\"copy_error\">Kopieringsfejl</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Moduler</string>\n    <string name=\"shizuku_available\">Shizuku service tilgængelig</string>\n    <string name=\"shizuku_unavailable\">Shizuku-tjenesten er ikke tilsluttet</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Logfiler</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Ukendt fejl</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Nogle funktioner er ikke tilgængelige</string>\n    <string name=\"home_api_version\">API-version</string>\n    <string name=\"home_lspatch_version\">LSPatch version</string>\n    <string name=\"home_framework_version\">Rammeversion</string>\n    <string name=\"home_system_version\">Systemversion</string>\n    <string name=\"home_device\">Enhed</string>\n    <string name=\"home_system_abi\">System ABI</string>\n    <string name=\"home_info_copied\">Kopieret til udklipsholder</string>\n    <string name=\"home_support\">Support</string>\n    <string name=\"home_description\">LSPatch er en gratis ikke-root Xposed-ramme baseret på LSPosed-kerne.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Se kildekoden på %1$s<br/>Deltag i vores %2$s kanal]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Administrer</string>\n    <string name=\"manage_loading\">Indlæser</string>\n    <string name=\"manage_no_apps\">Ingen lappede apps endnu</string>\n    <string name=\"manage_rolling\">Rullende</string>\n    <string name=\"manage_update_loader\">Opdatering af loader</string>\n    <string name=\"manage_update_loader_successfully\">Opdatering lykkedes</string>\n    <string name=\"manage_update_loader_failed\">Opdatering mislykkedes</string>\n    <string name=\"manage_module_scope\">Modulets anvendelsesområde</string>\n    <string name=\"manage_optimize\">Optimering af</string>\n    <string name=\"manage_optimize_successfully\">Optimering med succes</string>\n    <string name=\"manage_optimize_failed\">Optimering af mislykkedes</string>\n    <string name=\"manage_uninstall_successfully\">Afinstaller med succes</string>\n    <string name=\"manage_no_modules\">Ingen moduler endnu</string>\n    <string name=\"manage_module_settings\">Modulindstillinger</string>\n    <string name=\"manage_app_info\">App-info</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Ny patch</string>\n    <string name=\"patch_select_dir_title\">Vælg lagerbibliotek</string>\n    <string name=\"patch_select_dir_text\">Vælg en mappe til at gemme de patchede apks</string>\n    <string name=\"patch_select_dir_error\">Fejl ved indstilling af lagerbibliotek</string>\n    <string name=\"patch_from_storage\">Vælg apk(er) fra lageret</string>\n    <string name=\"patch_from_applist\">Vælg en installeret app</string>\n    <string name=\"patch_mode\">Patch-tilstand</string>\n    <string name=\"patch_local\">Lokal</string>\n    <string name=\"patch_local_desc\">Patch en app uden indlejrede moduler.\\nXposed scope kan ændres dynamisk uden re-patch.\\nLokalt patchede apps kan kun køre på den lokale enhed.</string>\n    <string name=\"patch_integrated\">Integreret</string>\n    <string name=\"patch_integrated_desc\">Patch en app med indlejrede moduler.\\nDen patchede app kan køre uden manageren, men kan ikke administreres dynamisk.\\nIntegrerede patchede apps kan bruges på enheder, der ikke har LSPatch Manager installeret.</string>\n    <string name=\"patch_embed_modules\">Indlejre moduler</string>\n    <string name=\"patch_debuggable\">Debuggable</string>\n    <string name=\"patch_sigbypass\">Signatur bypass</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Slukket</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Omgå PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Tilsidesæt versionskode</string>\n    <string name=\"patch_override_version_code_desc\">Overskrive den patchede apps versionskode til 1\\nDette giver mulighed for at nedgradere installationen i fremtiden, og generelt vil det ikke påvirke den versionskode, som programmet faktisk opfatter.</string>\n    <string name=\"patch_start\">Start patch</string>\n    <string name=\"patch_return\">Vend tilbage</string>\n    <string name=\"patch_uninstall_text\">På grund af forskellige signaturer skal du afinstallere den originale app, før du installerer den patchede.\\nSørg for, at du har sikkerhedskopieret personlige data.</string>\n    <string name=\"patch_install_successfully\">Installer med succes</string>\n    <string name=\"patch_install_failed\">Installationen mislykkedes</string>\n    <string name=\"patch_no_xposed_module\">Ingen Xposed-modul(er) blev fundet</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Vælg apps</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Indstillinger</string>\n    <string name=\"settings_keystore\">Signatur-nøgleopbevaring</string>\n    <string name=\"settings_keystore_default\">Indbygget</string>\n    <string name=\"settings_keystore_custom\">Tilpasset</string>\n    <string name=\"settings_keystore_dialog_title\">Brugerdefineret keystore</string>\n    <string name=\"settings_keystore_file\">Keystore-fil</string>\n    <string name=\"settings_keystore_password\">Adgangskode</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias-adgangskode</string>\n    <string name=\"settings_keystore_desc\">Indstil nøgleopbevaring (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Forkert type nøgleopbevaring</string>\n    <string name=\"settings_keystore_wrong_password\">Forkert adgangskode til keystore</string>\n    <string name=\"settings_keystore_wrong_alias\">Forkert aliasnavn</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Forkert adgangskode til alias</string>\n    <string name=\"settings_detail_patch_logs\">Detaljerede patch-logfiler</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Hinzufügen</string>\n    <string name=\"install\">Installieren</string>\n    <string name=\"installing\">Installieren</string>\n    <string name=\"uninstall\">Deinstallieren</string>\n    <string name=\"uninstalling\">Deinstallieren</string>\n    <string name=\"copy_error\">Fehler kopieren</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Module</string>\n    <string name=\"shizuku_available\">Shizuku-Dienst verfügbar</string>\n    <string name=\"shizuku_unavailable\">Shizuku-Dienst nicht verbunden</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Protokolle</string>\n    <string name=\"off\">Aus</string>\n    <string name=\"error_unknown\">Unbekannter Fehler</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Einige Funktionen nicht verfügbar</string>\n    <string name=\"home_api_version\">API-Version</string>\n    <string name=\"home_lspatch_version\">LSPatch-Version</string>\n    <string name=\"home_framework_version\">Framework-Version</string>\n    <string name=\"home_system_version\">Systemversion</string>\n    <string name=\"home_device\">Gerät</string>\n    <string name=\"home_system_abi\">System-ABI</string>\n    <string name=\"home_info_copied\">In Zwischenablage kopiert</string>\n    <string name=\"home_support\">Unterstützung</string>\n    <string name=\"home_description\">LSPatch ist ein kostenloses Xposed-Framework ohne Rootberechtigung, das auf den LSPosed-Kern basiert.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Quellcode unter %1$s anzeigen<br/>Trete unserem %2$s -Kanal bei]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Verwalten</string>\n    <string name=\"manage_loading\">Laden</string>\n    <string name=\"manage_no_apps\">Noch keine gepatchten Apps</string>\n    <string name=\"manage_rolling\">Rollen</string>\n    <string name=\"manage_update_loader\">Lader aktualisieren</string>\n    <string name=\"manage_update_loader_successfully\">Aktualisierung erfolgreich</string>\n    <string name=\"manage_update_loader_failed\">Aktualisierung fehlgeschlagen</string>\n    <string name=\"manage_module_scope\">Modulumfang</string>\n    <string name=\"manage_optimize\">Optimierung</string>\n    <string name=\"manage_optimize_successfully\">Optimierung erfolgreich</string>\n    <string name=\"manage_optimize_failed\">Optimierung fehlgeschlagen</string>\n    <string name=\"manage_uninstall_successfully\">Deinstallation erfolgreich</string>\n    <string name=\"manage_no_modules\">Noch keine Module</string>\n    <string name=\"manage_module_settings\">Modul-Einstellungen</string>\n    <string name=\"manage_app_info\">App-Info</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Neuer Patch</string>\n    <string name=\"patch_select_dir_title\">Speicherverzeichnis auswählen</string>\n    <string name=\"patch_select_dir_text\">Verzeichnis zum Speichern der gepatchten Apks auswählen</string>\n    <string name=\"patch_select_dir_error\">Fehler beim Festlegen des Speicherverzeichnisses</string>\n    <string name=\"patch_from_storage\">Wählen Sie apk(s) aus dem Speicher aus</string>\n    <string name=\"patch_from_applist\">Wählen Sie eine installierte App aus</string>\n    <string name=\"patch_mode\">Patch-Modus</string>\n    <string name=\"patch_local\">Lokal</string>\n    <string name=\"patch_local_desc\">Patchen Sie eine App ohne eingebettete Module.\\nDer Xposed-Bereich kann dynamisch geändert werden, ohne dass ein neuer Patch erforderlich ist.\\nLokal gepatchte Apps können nur auf dem lokalen Gerät ausgeführt werden.</string>\n    <string name=\"patch_integrated\">Integriert</string>\n    <string name=\"patch_integrated_desc\">Patche eine App mit eingebetteten Modulen.\\nDie gepatchte App kann ohne den Manager ausgeführt, aber nicht dynamisch verwaltet werden.\\nIntegrierte gepatchte Apps können auf Geräten verwendet werden, auf denen der LSPatch Manager nicht installiert ist.</string>\n    <string name=\"patch_embed_modules\">Module einbetten</string>\n    <string name=\"patch_debuggable\">Debuggingfähig</string>\n    <string name=\"patch_sigbypass\">Signaturumgehung</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Aus</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM umgehen</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PM umgehen + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Versionscode überschreiben</string>\n    <string name=\"patch_override_version_code_desc\">Überschreiben Sie den Versionscode der gepatchten Anwendung auf 1\\nDies ermöglicht eine Downgrade-Installation in der Zukunft und hat im Allgemeinen keinen Einfluss auf den Versionscode, der von der Anwendung tatsächlich wahrgenommen wird</string>\n    <string name=\"patch_start\">Patch starten</string>\n    <string name=\"patch_return\">Zurückkehren</string>\n    <string name=\"patch_uninstall_text\">Aufgrund unterschiedlicher Signaturen müssen Sie die Original-App deinstallieren, bevor Sie die gepatchte App installieren.\\nStellen Sie sicher, dass Sie Ihre persönlichen Daten gesichert haben.</string>\n    <string name=\"patch_install_successfully\">Installation erfolgreich</string>\n    <string name=\"patch_install_failed\">Installation fehlgeschlagen</string>\n    <string name=\"patch_no_xposed_module\">Es wurde(n) kein(e) Xposed-Modul(e) gefunden</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Apps auswählen</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Einstellungen</string>\n    <string name=\"settings_keystore\">Signatur-Schlüsselspeicher</string>\n    <string name=\"settings_keystore_default\">Eingebaut</string>\n    <string name=\"settings_keystore_custom\">Benutzerdefiniert</string>\n    <string name=\"settings_keystore_dialog_title\">Benutzerdefinierter Schlüsselspeicher</string>\n    <string name=\"settings_keystore_file\">Schlüsselspeicher-Datei</string>\n    <string name=\"settings_keystore_password\">Passwort</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias-Passwort</string>\n    <string name=\"settings_keystore_desc\">Schlüsselspeicher festlegen (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Falscher Typ des Schlüsselspeichers</string>\n    <string name=\"settings_keystore_wrong_password\">Falsches Schlüsselspeicher-Passwort</string>\n    <string name=\"settings_keystore_wrong_alias\">Falscher Alias-Name</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Falsches Alias-Passwort</string>\n    <string name=\"settings_detail_patch_logs\">Detaillierte Patch-Protokolle</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-el/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Προσθήκη</string>\n    <string name=\"install\">Εγκαταστήστε το</string>\n    <string name=\"installing\">Εγκατάσταση του</string>\n    <string name=\"uninstall\">Απεγκατάσταση του</string>\n    <string name=\"uninstalling\">Απεγκατάσταση του</string>\n    <string name=\"copy_error\">Σφάλμα αντιγραφής</string>\n    <string name=\"apps\">Εφαρμογές</string>\n    <string name=\"modules\">Ενότητες</string>\n    <string name=\"shizuku_available\">Διατίθεται υπηρεσία Shizuku</string>\n    <string name=\"shizuku_unavailable\">Η υπηρεσία Shizuku δεν είναι συνδεδεμένη</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Ημερολόγια</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Άγνωστο σφάλμα</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Ορισμένες λειτουργίες δεν είναι διαθέσιμες</string>\n    <string name=\"home_api_version\">Έκδοση API</string>\n    <string name=\"home_lspatch_version\">Έκδοση LSPatch</string>\n    <string name=\"home_framework_version\">Έκδοση πλαισίου</string>\n    <string name=\"home_system_version\">Έκδοση συστήματος</string>\n    <string name=\"home_device\">Συσκευή</string>\n    <string name=\"home_system_abi\">Σύστημα ABI</string>\n    <string name=\"home_info_copied\">Αντιγράφηκε στο πρόχειρο</string>\n    <string name=\"home_support\">Υποστήριξη</string>\n    <string name=\"home_description\">Το LSPatch είναι ένα δωρεάν πλαίσιο Xposed χωρίς ρίζα που βασίζεται στον πυρήνα LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Δείτε τον πηγαίο κώδικα στο %1$s<br/>Εγγραφείτε στο %2$s κανάλι μας]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Διαχείριση</string>\n    <string name=\"manage_loading\">Φόρτωση</string>\n    <string name=\"manage_no_apps\">Δεν υπάρχουν ακόμα επιδιορθωμένες εφαρμογές</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Ενημέρωση φορτωτή</string>\n    <string name=\"manage_update_loader_successfully\">Ενημέρωση με επιτυχία</string>\n    <string name=\"manage_update_loader_failed\">Η ενημέρωση απέτυχε</string>\n    <string name=\"manage_module_scope\">Πεδίο εφαρμογής της ενότητας</string>\n    <string name=\"manage_optimize\">Βελτιστοποίηση</string>\n    <string name=\"manage_optimize_successfully\">Βελτιστοποιήστε με επιτυχία</string>\n    <string name=\"manage_optimize_failed\">Βελτιστοποίηση αποτυχημένων</string>\n    <string name=\"manage_uninstall_successfully\">Απεγκατάσταση με επιτυχία</string>\n    <string name=\"manage_no_modules\">Δεν υπάρχουν ακόμη ενότητες</string>\n    <string name=\"manage_module_settings\">Ρυθμίσεις ενότητας</string>\n    <string name=\"manage_app_info\">Πληροφορίες εφαρμογής</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Νέο patch</string>\n    <string name=\"patch_select_dir_title\">Επιλέξτε κατάλογο αποθήκευσης</string>\n    <string name=\"patch_select_dir_text\">Επιλέξτε έναν κατάλογο για να αποθηκεύσετε τα επιδιορθωμένα apk</string>\n    <string name=\"patch_select_dir_error\">Σφάλμα κατά τη ρύθμιση του καταλόγου αποθήκευσης</string>\n    <string name=\"patch_from_storage\">Επιλέξτε apk από την αποθήκευση</string>\n    <string name=\"patch_from_applist\">Επιλέξτε μια εγκατεστημένη εφαρμογή</string>\n    <string name=\"patch_mode\">Λειτουργία ενημέρωσης κώδικα</string>\n    <string name=\"patch_local\">Τοπικός</string>\n    <string name=\"patch_local_desc\">Επιδιόρθωση μιας εφαρμογής χωρίς ενσωματωμένες ενότητες.\\nΤο πεδίο εφαρμογής του Xposed μπορεί να αλλάξει δυναμικά χωρίς επαναπατρισμό.\\nΟι τοπικές επιδιορθωμένες εφαρμογές μπορούν να εκτελούνται μόνο στην τοπική συσκευή.</string>\n    <string name=\"patch_integrated\">Ολοκληρωμένο</string>\n    <string name=\"patch_integrated_desc\">Επιδιορθώστε μια εφαρμογή με ενσωματωμένες λειτουργικές μονάδες.\\nΗ επιδιορθωμένη εφαρμογή μπορεί να εκτελεστεί χωρίς τον διαχειριστή, αλλά δεν είναι δυνατή η δυναμική διαχείριση της.\\nΟι ενσωματωμένες ενημερωμένες εφαρμογές μπορούν να χρησιμοποιηθούν σε συσκευές που δεν έχουν εγκατεστημένο το LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Ενσωμάτωση λειτουργικών μονάδων</string>\n    <string name=\"patch_debuggable\">Δυνατότητα εντοπισμού σφαλμάτων</string>\n    <string name=\"patch_sigbypass\">Παράκαμψη υπογραφής</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Ανενεργό</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Παράκαμψη PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Παράκαμψη PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Παράκαμψη κωδικού έκδοσης</string>\n    <string name=\"patch_override_version_code_desc\">Αντικαταστήστε τον κωδικό έκδοσης της επιδιορθωμένης εφαρμογής σε 1\\nΑυτό επιτρέπει την εγκατάσταση υποβάθμισης στο μέλλον και γενικά αυτό δεν θα επηρεάσει τον κωδικό έκδοσης που αντιλαμβάνεται πραγματικά η εφαρμογή.</string>\n    <string name=\"patch_start\">Ξεκινήστε το Patch</string>\n    <string name=\"patch_return\">ΕΠΙΣΤΡΟΦΗ</string>\n    <string name=\"patch_uninstall_text\">Λόγω διαφορετικών υπογραφών, πρέπει να απεγκαταστήσετε την αρχική εφαρμογή πριν εγκαταστήσετε την επιδιορθωμένη.\\nΒεβαιωθείτε ότι έχετε δημιουργήσει αντίγραφα ασφαλείας προσωπικών δεδομένων.</string>\n    <string name=\"patch_install_successfully\">Εγκατάσταση με επιτυχία</string>\n    <string name=\"patch_install_failed\">Η εγκατάσταση απέτυχε</string>\n    <string name=\"patch_no_xposed_module\">Δεν βρέθηκαν μονάδες Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Επιλέξτε εφαρμογές</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Ρυθμίσεις</string>\n    <string name=\"settings_keystore\">Υπογραφή keystore</string>\n    <string name=\"settings_keystore_default\">Ενσωματωμένο</string>\n    <string name=\"settings_keystore_custom\">Προσαρμοσμένο</string>\n    <string name=\"settings_keystore_dialog_title\">Προσαρμοσμένο keystore</string>\n    <string name=\"settings_keystore_file\">Αρχείο κλειδοθήκης</string>\n    <string name=\"settings_keystore_password\">Κωδικός πρόσβασης</string>\n    <string name=\"settings_keystore_alias\">Ψευδώνυμο</string>\n    <string name=\"settings_keystore_alias_password\">Κωδικός πρόσβασης Alias</string>\n    <string name=\"settings_keystore_desc\">Ορισμός κλειδοθήκης (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Λάθος τύπος κλειδοθήκης</string>\n    <string name=\"settings_keystore_wrong_password\">Λάθος κωδικός πρόσβασης keystore</string>\n    <string name=\"settings_keystore_wrong_alias\">Λάθος όνομα ψευδώνυμου</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Λάθος κωδικός πρόσβασης ψευδώνυμου</string>\n    <string name=\"settings_detail_patch_logs\">Λεπτομερή αρχεία καταγραφής επιδιορθώσεων</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Agregar</string>\n    <string name=\"install\">Instalar</string>\n    <string name=\"installing\">Instalación de</string>\n    <string name=\"uninstall\">Desinstalar</string>\n    <string name=\"uninstalling\">Desinstalación de</string>\n    <string name=\"copy_error\">Error de copia</string>\n    <string name=\"apps\">Aplicaciones</string>\n    <string name=\"modules\">Módulos</string>\n    <string name=\"shizuku_available\">Servicio Shizuku disponible</string>\n    <string name=\"shizuku_unavailable\">Servicio Shizuku no conectado</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Registros</string>\n    <string name=\"off\">Fuera de</string>\n    <string name=\"error_unknown\">Error desconocido</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Algunas funciones no disponibles</string>\n    <string name=\"home_api_version\">Versión API</string>\n    <string name=\"home_lspatch_version\">Versión de LSPatch</string>\n    <string name=\"home_framework_version\">Versión del marco</string>\n    <string name=\"home_system_version\">Versión del sistema</string>\n    <string name=\"home_device\">Dispositivo</string>\n    <string name=\"home_system_abi\">Sistema ABI</string>\n    <string name=\"home_info_copied\">Copiado al portapapeles</string>\n    <string name=\"home_support\">Apoyo</string>\n    <string name=\"home_description\">LSPatch es un marco Xposed no root gratuito basado en el núcleo LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Ver código fuente en %1$s<br/>Únete a nuestro canal %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gestionar</string>\n    <string name=\"manage_loading\">Cargando</string>\n    <string name=\"manage_no_apps\">Aún no hay aplicaciones parcheadas</string>\n    <string name=\"manage_rolling\">Rodando</string>\n    <string name=\"manage_update_loader\">Actualizar el cargador</string>\n    <string name=\"manage_update_loader_successfully\">Actualizar con éxito</string>\n    <string name=\"manage_update_loader_failed\">Actualización fallida</string>\n    <string name=\"manage_module_scope\">Alcance del módulo</string>\n    <string name=\"manage_optimize\">Optimizar</string>\n    <string name=\"manage_optimize_successfully\">Optimizar con éxito</string>\n    <string name=\"manage_optimize_failed\">Optimización fallida</string>\n    <string name=\"manage_uninstall_successfully\">Desinstalar con éxito</string>\n    <string name=\"manage_no_modules\">Todavía no hay módulos</string>\n    <string name=\"manage_module_settings\">Ajustes del módulo</string>\n    <string name=\"manage_app_info\">Información de la aplicación</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nuevo parche</string>\n    <string name=\"patch_select_dir_title\">Seleccionar directorio de almacenamiento</string>\n    <string name=\"patch_select_dir_text\">Seleccione un directorio para almacenar las aplicaciones parcheadas</string>\n    <string name=\"patch_select_dir_error\">Error al configurar el directorio de almacenamiento</string>\n    <string name=\"patch_from_storage\">Seleccionar apk(s) del almacenamiento</string>\n    <string name=\"patch_from_applist\">Seleccione una aplicación instalada</string>\n    <string name=\"patch_mode\">Modo de parche</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Parche de una aplicación sin módulos incrustados.\\nEl ámbito de Xposed puede cambiarse dinámicamente sin necesidad de volver a parchear.\\nLas aplicaciones locales parcheadas solo pueden ejecutarse en el dispositivo local.</string>\n    <string name=\"patch_integrated\">Integrado</string>\n    <string name=\"patch_integrated_desc\">Parchea una app con módulos incrustados.\\nLa aplicación parcheada puede ejecutarse sin el gestor, pero no puede gestionarse dinámicamente.\\nLas apps parcheadas integradas pueden utilizarse en dispositivos que no tengan instalado LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Módulos incrustados</string>\n    <string name=\"patch_debuggable\">depurable</string>\n    <string name=\"patch_sigbypass\">Omisión de firma</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Desactivado</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Pasar PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Omitir PM + abrir en (libc)</string>\n    <string name=\"patch_override_version_code\">Anular código de versión</string>\n    <string name=\"patch_override_version_code_desc\">Anular el código de versión de la app parcheada a 1\\nEsto permite la instalación de downgrade en el futuro, y generalmente esto no afectará al código de versión realmente percibido por la aplicación</string>\n    <string name=\"patch_start\">Parche de inicio</string>\n    <string name=\"patch_return\">Regreso</string>\n    <string name=\"patch_uninstall_text\">Debido a las diferentes firmas, debe desinstalar la aplicación original antes de instalar la parcheada.\\nAsegúrese de haber realizado una copia de seguridad de los datos personales.</string>\n    <string name=\"patch_install_successfully\">Instalar con éxito</string>\n    <string name=\"patch_install_failed\">Instalación fallida</string>\n    <string name=\"patch_no_xposed_module\">No se han encontrado módulos Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Seleccionar aplicaciones</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Ajustes</string>\n    <string name=\"settings_keystore\">Almacén de claves de firma</string>\n    <string name=\"settings_keystore_default\">Construido en</string>\n    <string name=\"settings_keystore_custom\">Personalizado</string>\n    <string name=\"settings_keystore_dialog_title\">Almacén de claves personalizado</string>\n    <string name=\"settings_keystore_file\">Archivo Keystore</string>\n    <string name=\"settings_keystore_password\">Contraseña</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Contraseña del alias</string>\n    <string name=\"settings_keystore_desc\">Establecer el almacén de claves (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Tipo de almacén de claves incorrecto</string>\n    <string name=\"settings_keystore_wrong_password\">Contraseña incorrecta del almacén de claves</string>\n    <string name=\"settings_keystore_wrong_alias\">Nombre de alias incorrecto</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Contraseña de alias incorrecta</string>\n    <string name=\"settings_detail_patch_logs\">Registros de parches detallados</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Lisama</string>\n    <string name=\"install\">Installige</string>\n    <string name=\"installing\">Paigaldamine</string>\n    <string name=\"uninstall\">Desinstallige</string>\n    <string name=\"uninstalling\">Desinstallimine</string>\n    <string name=\"copy_error\">Kopeerimise viga</string>\n    <string name=\"apps\">Rakendused</string>\n    <string name=\"modules\">Moodulid</string>\n    <string name=\"shizuku_available\">Shizuku teenus saadaval</string>\n    <string name=\"shizuku_unavailable\">Shizuku teenus pole ühendatud</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Palgid</string>\n    <string name=\"off\">Väljas</string>\n    <string name=\"error_unknown\">Teadmata viga</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Mõned funktsioonid pole saadaval</string>\n    <string name=\"home_api_version\">API versioon</string>\n    <string name=\"home_lspatch_version\">LSPatchi versioon</string>\n    <string name=\"home_framework_version\">Raamversioon</string>\n    <string name=\"home_system_version\">Süsteemi versioon</string>\n    <string name=\"home_device\">Seade</string>\n    <string name=\"home_system_abi\">Süsteemi ABI</string>\n    <string name=\"home_info_copied\">Kopeeriti lõikelauale</string>\n    <string name=\"home_support\">Toetus</string>\n    <string name=\"home_description\">LSPatch on tasuta mittejuur-Xposedi raamistik, mis põhineb LSPosedi tuumal.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Vaadake lähtekoodi aadressil %1$s<br/>Liituge meie %2$s kanaliga]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Halda</string>\n    <string name=\"manage_loading\">Laadimine</string>\n    <string name=\"manage_no_apps\">Paigutatud rakendusi pole veel</string>\n    <string name=\"manage_rolling\">Veeremine</string>\n    <string name=\"manage_update_loader\">Värskenda laadurit</string>\n    <string name=\"manage_update_loader_successfully\">Värskendamine õnnestus</string>\n    <string name=\"manage_update_loader_failed\">Uuendus ebaõnnestus</string>\n    <string name=\"manage_module_scope\">Mooduli ulatus</string>\n    <string name=\"manage_optimize\">Optimeerige</string>\n    <string name=\"manage_optimize_successfully\">Optimeerimine õnnestus</string>\n    <string name=\"manage_optimize_failed\">Optimeerimine ebaõnnestus</string>\n    <string name=\"manage_uninstall_successfully\">Desinstallimine õnnestus</string>\n    <string name=\"manage_no_modules\">Mooduleid veel pole</string>\n    <string name=\"manage_module_settings\">Mooduli seaded</string>\n    <string name=\"manage_app_info\">Rakenduse teave</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Uus plaaster</string>\n    <string name=\"patch_select_dir_title\">Valige salvestuskataloog</string>\n    <string name=\"patch_select_dir_text\">Valige kataloog, kuhu paigatud APK-d salvestada</string>\n    <string name=\"patch_select_dir_error\">Viga salvestuskataloogi seadistamisel</string>\n    <string name=\"patch_from_storage\">Valige mälust apk(id).</string>\n    <string name=\"patch_from_applist\">Valige installitud rakendus</string>\n    <string name=\"patch_mode\">Paigutuse režiim</string>\n    <string name=\"patch_local\">Kohalik</string>\n    <string name=\"patch_local_desc\">Paigaldage rakendus ilma mooduliteta sisseehitatud.\\nXposed ulatust saab dünaamiliselt muuta ilma uuesti paranduseta.\\nKohalikud parandatud rakendused saavad töötada ainult kohalikus seadmes.</string>\n    <string name=\"patch_integrated\">Integreeritud</string>\n    <string name=\"patch_integrated_desc\">Paigutage manustatud moodulitega rakendust.\\nPaigutatud rakendus võib töötada ilma haldurita, kuid seda ei saa dünaamiliselt hallata.\\nIntegreeritud paigatud rakendusi saab kasutada seadmetes, kuhu pole installitud LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Manustage moodulid</string>\n    <string name=\"patch_debuggable\">Silutav</string>\n    <string name=\"patch_sigbypass\">Allkirjast möödasõit</string>\n    <string name=\"patch_sigbypasslv0\">lv0: väljas</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM ümbersõit</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PM + openat (libc) ümbersõit</string>\n    <string name=\"patch_override_version_code\">Alista versioonikood</string>\n    <string name=\"patch_override_version_code_desc\">Paigutatud rakenduse versioonikoodi alistamine väärtusele 1\\nSee võimaldab edaspidi installida madalamale versioonile ja üldiselt ei mõjuta see rakenduse poolt tegelikult tajutavat versioonikoodi</string>\n    <string name=\"patch_start\">Käivitage patch</string>\n    <string name=\"patch_return\">Tagasi</string>\n    <string name=\"patch_uninstall_text\">Erinevate allkirjade tõttu peate enne paigatud rakenduse installimist algse rakenduse desinstallima.\\nVeenduge, et olete isikuandmed varundanud.</string>\n    <string name=\"patch_install_successfully\">Installimine õnnestus</string>\n    <string name=\"patch_install_failed\">Install ebaõnnestus</string>\n    <string name=\"patch_no_xposed_module\">Xposed moodul(id) ei leitud</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Valige Rakendused</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Seaded</string>\n    <string name=\"settings_keystore\">Allkirja võtmehoidla</string>\n    <string name=\"settings_keystore_default\">Sisseehitatud</string>\n    <string name=\"settings_keystore_custom\">Kohandatud</string>\n    <string name=\"settings_keystore_dialog_title\">Kohandatud võtmehoidla</string>\n    <string name=\"settings_keystore_file\">Võtmehoidla fail</string>\n    <string name=\"settings_keystore_password\">Parool</string>\n    <string name=\"settings_keystore_alias\">Teise nimega</string>\n    <string name=\"settings_keystore_alias_password\">Alias parool</string>\n    <string name=\"settings_keystore_desc\">Määra võtmehoidja (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Vale võtmehoidja tüüp</string>\n    <string name=\"settings_keystore_wrong_password\">Vale võtmehoidla parool</string>\n    <string name=\"settings_keystore_wrong_alias\">Vale pseudonüümi nimi</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Vale aliase parool</string>\n    <string name=\"settings_detail_patch_logs\">Üksikasjade paikade logid</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">افزودن</string>\n    <string name=\"install\">نصب</string>\n    <string name=\"installing\">در حال نصب ...</string>\n    <string name=\"uninstall\">حذف نصب</string>\n    <string name=\"uninstalling\">در حال حذف نصب ...</string>\n    <string name=\"copy_error\">خطا کپی شود</string>\n    <string name=\"apps\">برنامه ها</string>\n    <string name=\"modules\">ماژول ها</string>\n    <string name=\"shizuku_available\">سرویس Shizuku در دسترس‌ است</string>\n    <string name=\"shizuku_unavailable\">سرویس Shizuku متصل نیست</string>\n    <string name=\"screen_repo\">مخزن</string>\n    <string name=\"screen_logs\">گزارش ها</string>\n    <string name=\"off\">خاموش</string>\n    <string name=\"error_unknown\"></string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">برخی از عملکردها در دسترس نیستند</string>\n    <string name=\"home_api_version\">نسخه API</string>\n    <string name=\"home_lspatch_version\">نسخه LSPatch</string>\n    <string name=\"home_framework_version\">نسخه فریمورک</string>\n    <string name=\"home_system_version\">نسخه اندروید</string>\n    <string name=\"home_device\">مدل دستگاه</string>\n    <string name=\"home_system_abi\">نوع پردازنده</string>\n    <string name=\"home_info_copied\">در کلیپ بورد کپی شده</string>\n    <string name=\"home_support\">حمایت</string>\n    <string name=\"home_description\">LSPatch یک فریمورک Xposed بدون نیاز به روت رایگان بر اساس هسته LSPosed است</string>\n    <string name=\"home_view_source_code\"><![CDATA[کد منبع را در %1$s مشاهده کنید<br/>و به کانال %2$s ما بپیوندید]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">مدیریت</string>\n    <string name=\"manage_loading\">در حال بارگذاری ...</string>\n    <string name=\"manage_no_apps\">هنوز هیچ برنامه ای پچ نشده</string>\n    <string name=\"manage_rolling\">متحرک</string>\n    <string name=\"manage_update_loader\">به روز رسانی بارگذاری کننده</string>\n    <string name=\"manage_update_loader_successfully\">به روز رسانی انجام شده</string>\n    <string name=\"manage_update_loader_failed\">به روز رسانی انجام نشده</string>\n    <string name=\"manage_module_scope\">محدوده ماژول</string>\n    <string name=\"manage_optimize\">بهینه سازی</string>\n    <string name=\"manage_optimize_successfully\">بهینه سازی انجام شده</string>\n    <string name=\"manage_optimize_failed\">بهینه سازی انجام نشده</string>\n    <string name=\"manage_uninstall_successfully\">حذف نصب شده</string>\n    <string name=\"manage_no_modules\">هنوز ماژولی وجود ندارد</string>\n    <string name=\"manage_module_settings\">تنظیمات ماژول</string>\n    <string name=\"manage_app_info\">اطلاعات برنامه</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">پچ جدید</string>\n    <string name=\"patch_select_dir_title\">پوشه ذخیره سازی را انتخاب کنید</string>\n    <string name=\"patch_select_dir_text\">یک پوشه را برای ذخیره apk های پچ شده انتخاب کنید</string>\n    <string name=\"patch_select_dir_error\">خطا در هنگام تنظیم پوشه ذخیره سازی</string>\n    <string name=\"patch_from_storage\">انتخاب فایل (های) apk از حافظه </string>\n    <string name=\"patch_from_applist\">انتخاب یک برنامه نصب شده</string>\n    <string name=\"patch_mode\">حالت پچ</string>\n    <string name=\"patch_local\">محلی</string>\n    <string name=\"patch_local_desc\">یک برنامه بدون ماژول های تعبیه شده را وصله کنید.\\nمحدوده Xposed را می توان به صورت پویا بدون وصله مجدد تغییر داد.\\nبرنامه های وصله شده محلی فقط می توانند در دستگاه محلی اجرا شوند.</string>\n    <string name=\"patch_integrated\">یکپارچه</string>\n    <string name=\"patch_integrated_desc\">یک برنامه را با ماژول های تعبیه شده پچ کنید برنامه پچ شده می‌تواند بدون نیاز مدیر اجرا شود اما نمی توان آن را به‌ صورت پویا مدیریت کرد برنامه‌های پچ شده را می‌توان در دستگاه‌ هایی که مدیر LSPatch در آن نصب نشده استفاده کرد</string>\n    <string name=\"patch_embed_modules\">جاسازی ماژول ها</string>\n    <string name=\"patch_debuggable\">قابل اشکال زدایی</string>\n    <string name=\"patch_sigbypass\">دور زدن امضا</string>\n    <string name=\"patch_sigbypasslv0\">سطح 0 : خاموش</string>\n    <string name=\"patch_sigbypasslv1\">سطح ۱ : دور زدن PM</string>\n    <string name=\"patch_sigbypasslv2\">سطح ۲ : دور زدن PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">لغو کد نسخه</string>\n    <string name=\"patch_override_version_code_desc\">کد نسخه برنامه پچ شده را به ۱ تغییر می دهد این کار امکان بازگردانی نسخه قبلی را فراهم می کند به طور کلی این کار روی کد نسخه ای که توسط برنامه درک می شود تاثیر نمی گذارد</string>\n    <string name=\"patch_start\">شروع پچ</string>\n    <string name=\"patch_return\">بازیابی</string>\n    <string name=\"patch_uninstall_text\">به دلیل انجام امضاهای مختلف باید قبل از نصب برنامه پچ شده برنامه اصلی را حذف نصب کنید لطفا قبل از انجام این کار از اطلاعات خود پشتیبان گیری کنید</string>\n    <string name=\"patch_install_successfully\">نصب شده</string>\n    <string name=\"patch_install_failed\">نصب نشده</string>\n    <string name=\"patch_no_xposed_module\">هیچ ماژول(های) Xposed یافت نشد</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">برنامه ها را انتخاب کنید</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">تنظیمات</string>\n    <string name=\"settings_keystore\">فروشگاه کلید امضا</string>\n    <string name=\"settings_keystore_default\">ساخته شده در</string>\n    <string name=\"settings_keystore_custom\">سفارشی</string>\n    <string name=\"settings_keystore_dialog_title\">فروشگاه کلید سفارشی</string>\n    <string name=\"settings_keystore_file\">فایل فروشگاه کلید</string>\n    <string name=\"settings_keystore_password\">رمز</string>\n    <string name=\"settings_keystore_alias\">نام مستعار</string>\n    <string name=\"settings_keystore_alias_password\">رمز مستعار</string>\n    <string name=\"settings_keystore_desc\">تنظیم فروشگاه کلید (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">نوع اشتباه ذخیره کلید</string>\n    <string name=\"settings_keystore_wrong_password\">رمز فروشگاه کلید اشتباه است</string>\n    <string name=\"settings_keystore_wrong_alias\">نام مستعار اشتباه است</string>\n    <string name=\"settings_keystore_wrong_alias_password\">رمز یا نام مستعار اشتباه است</string>\n    <string name=\"settings_detail_patch_logs\">گزارش های اطلاعات پچ</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-fi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Lisätä</string>\n    <string name=\"install\">Asenna</string>\n    <string name=\"installing\">Asennus</string>\n    <string name=\"uninstall\">Poista</string>\n    <string name=\"uninstalling\">Asennuksen poistaminen</string>\n    <string name=\"copy_error\">Kopiointivirhe</string>\n    <string name=\"apps\">Sovellukset</string>\n    <string name=\"modules\">Moduulit</string>\n    <string name=\"shizuku_available\">Shizuku-palvelu saatavilla</string>\n    <string name=\"shizuku_unavailable\">Shizuku-palvelua ei ole yhdistetty</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Lokit</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Tuntematon virhe</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Jotkut toiminnot eivät ole käytettävissä</string>\n    <string name=\"home_api_version\">API-versio</string>\n    <string name=\"home_lspatch_version\">LSPatch-versio</string>\n    <string name=\"home_framework_version\">Framework-versio</string>\n    <string name=\"home_system_version\">Järjestelmän versio</string>\n    <string name=\"home_device\">Laite</string>\n    <string name=\"home_system_abi\">Järjestelmä ABI</string>\n    <string name=\"home_info_copied\">Kopioitu leikepöydälle</string>\n    <string name=\"home_support\">Tuki</string>\n    <string name=\"home_description\">LSPatch on ilmainen ei-root Xposed -kehys, joka perustuu LSPosed-ytimeen.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Katso lähdekoodi osoitteessa %1$s<br/>Liity %2$s kanavaamme]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Hallitse</string>\n    <string name=\"manage_loading\">Ladataan</string>\n    <string name=\"manage_no_apps\">Ei vielä korjattuja sovelluksia</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Päivitä lataaja</string>\n    <string name=\"manage_update_loader_successfully\">Päivitys onnistuneesti</string>\n    <string name=\"manage_update_loader_failed\">Päivitys epäonnistui</string>\n    <string name=\"manage_module_scope\">Moduulin laajuus</string>\n    <string name=\"manage_optimize\">Optimoi</string>\n    <string name=\"manage_optimize_successfully\">Optimoi onnistuneesti</string>\n    <string name=\"manage_optimize_failed\">Optimoi epäonnistunut</string>\n    <string name=\"manage_uninstall_successfully\">Poista asennus onnistuneesti</string>\n    <string name=\"manage_no_modules\">Ei vielä moduuleja</string>\n    <string name=\"manage_module_settings\">Moduulien asetukset</string>\n    <string name=\"manage_app_info\">Sovelluksen tiedot</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Uusi laastari</string>\n    <string name=\"patch_select_dir_title\">Valitse tallennushakemisto</string>\n    <string name=\"patch_select_dir_text\">Valitse hakemisto, johon tallennetaan korjatut APK:t</string>\n    <string name=\"patch_select_dir_error\">Virhe asetettaessa tallennushakemistoa</string>\n    <string name=\"patch_from_storage\">Valitse apk:t tallennustilasta</string>\n    <string name=\"patch_from_applist\">Valitse asennettu sovellus</string>\n    <string name=\"patch_mode\">Patch-tila</string>\n    <string name=\"patch_local\">Paikallinen</string>\n    <string name=\"patch_local_desc\">Korjaa sovellus ilman upotettuja moduuleja.\\nXposedin laajuutta voidaan muuttaa dynaamisesti ilman uudelleenpatchausta.\\nPaikalliset paikallistetut sovellukset voivat toimia vain paikallisella laitteella.</string>\n    <string name=\"patch_integrated\">Integroitu</string>\n    <string name=\"patch_integrated_desc\">Korjaa sovellus, jossa on upotettuja moduuleja.\\nKorjattu sovellus voi toimia ilman hallintaa, mutta sitä ei voida hallita dynaamisesti.\\nIntegroituja korjattuja sovelluksia voidaan käyttää laitteissa, joihin ei ole asennettu LSPatch Manageria.</string>\n    <string name=\"patch_embed_modules\">Upota moduulit</string>\n    <string name=\"patch_debuggable\">Virheenkorjaus</string>\n    <string name=\"patch_sigbypass\">Allekirjoituksen ohitus</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Pois</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Ohita PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Ohita PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Ohita versiokoodi</string>\n    <string name=\"patch_override_version_code_desc\">Korjatun sovelluksen versiokoodi korvataan arvolla 1\\nTämä mahdollistaa downgrade-asennuksen tulevaisuudessa, eikä tämä yleensä vaikuta sovelluksen havaitsemaan versiokoodiin.</string>\n    <string name=\"patch_start\">Aloita korjaustiedosto</string>\n    <string name=\"patch_return\">Palata</string>\n    <string name=\"patch_uninstall_text\">Erilaisten allekirjoitusten vuoksi sinun on poistettava alkuperäinen sovellus ennen korjatun sovelluksen asentamista.\\nVarmista, että olet varmuuskopioinut henkilökohtaiset tiedot.</string>\n    <string name=\"patch_install_successfully\">Asennus onnistui</string>\n    <string name=\"patch_install_failed\">Asennus epäonnistui</string>\n    <string name=\"patch_no_xposed_module\">Xposed-moduulia (-moduulia) ei löytynyt</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Valitse sovellukset</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Asetukset</string>\n    <string name=\"settings_keystore\">Allekirjoituksen avainsäilö</string>\n    <string name=\"settings_keystore_default\">Sisäänrakennettu</string>\n    <string name=\"settings_keystore_custom\">Custom</string>\n    <string name=\"settings_keystore_dialog_title\">Mukautettu avainsäilö</string>\n    <string name=\"settings_keystore_file\">Avainsäilytystiedosto</string>\n    <string name=\"settings_keystore_password\">Salasana</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias-salasana</string>\n    <string name=\"settings_keystore_desc\">Avainsäilön asettaminen (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Väärän tyyppinen avainsäilö</string>\n    <string name=\"settings_keystore_wrong_password\">Väärä avainsäilön salasana</string>\n    <string name=\"settings_keystore_wrong_alias\">Väärä alias-nimi</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Väärä alias-salasana</string>\n    <string name=\"settings_detail_patch_logs\">Yksityiskohtaiset korjauslokit</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Ajouter</string>\n    <string name=\"install\">Install</string>\n    <string name=\"installing\">Installation de</string>\n    <string name=\"uninstall\">Désinstaller</string>\n    <string name=\"uninstalling\">Désinstallation de</string>\n    <string name=\"copy_error\">Erreur de copie</string>\n    <string name=\"apps\">Applications</string>\n    <string name=\"modules\">Modules</string>\n    <string name=\"shizuku_available\">Service Shizuku disponible</string>\n    <string name=\"shizuku_unavailable\">Service Shizuku non connecté</string>\n    <string name=\"screen_repo\">Dépôt</string>\n    <string name=\"screen_logs\">Journaux</string>\n    <string name=\"off\">Arrêt</string>\n    <string name=\"error_unknown\">Erreur inconnue</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Certaines fonctionnalités seront indisponibles</string>\n    <string name=\"home_api_version\">Version de l\\'API</string>\n    <string name=\"home_lspatch_version\">Version de LSPatch</string>\n    <string name=\"home_framework_version\">Version du Framework</string>\n    <string name=\"home_system_version\">Version du système</string>\n    <string name=\"home_device\">Appareil</string>\n    <string name=\"home_system_abi\">Architecture du système</string>\n    <string name=\"home_info_copied\">Copié dans le presse-papier</string>\n    <string name=\"home_support\">Support</string>\n    <string name=\"home_description\">LSPatch est un framework Xposed gratuit non root basé sur le noyau de LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Voir le code source sur %1$s<br/>Rejoignez notre %2$s canal]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gérer</string>\n    <string name=\"manage_loading\">Chargement</string>\n    <string name=\"manage_no_apps\">Aucune appli patchée pour le moment</string>\n    <string name=\"manage_rolling\">Rouler</string>\n    <string name=\"manage_update_loader\">Mettre à jour le chargeur</string>\n    <string name=\"manage_update_loader_successfully\">Mise à jour réussie</string>\n    <string name=\"manage_update_loader_failed\">Échec de la mise à jour</string>\n    <string name=\"manage_module_scope\">Portée du module</string>\n    <string name=\"manage_optimize\">Optimiser</string>\n    <string name=\"manage_optimize_successfully\">Optimiser avec succès</string>\n    <string name=\"manage_optimize_failed\">Échec de l\\'optimisation</string>\n    <string name=\"manage_uninstall_successfully\">Désinstallation réussie</string>\n    <string name=\"manage_no_modules\">Aucun module pour le moment</string>\n    <string name=\"manage_module_settings\">Réglages du module</string>\n    <string name=\"manage_app_info\">Infos sur l’application</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nouveau Patch</string>\n    <string name=\"patch_select_dir_title\">Sélectionner le répertoire de stockage</string>\n    <string name=\"patch_select_dir_text\">Sélectionnez un répertoire pour stocker les apks modifiés</string>\n    <string name=\"patch_select_dir_error\">Érreur lors de la définition du répertoire de stockage</string>\n    <string name=\"patch_from_storage\">Selctionner un ou plusieurs apks depuis le stockage</string>\n    <string name=\"patch_from_applist\">Sélectionner une appli installée</string>\n    <string name=\"patch_mode\">Mode de patch</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Patch d\\'une application sans modules intégrés.\\nLe champ d\\'application de Xposed peut être modifié dynamiquement sans qu\\'il soit nécessaire d\\'appliquer un nouveau correctif.\\nLes applications patchées localement ne peuvent fonctionner que sur l\\'appareil local.</string>\n    <string name=\"patch_integrated\">Intégré</string>\n    <string name=\"patch_integrated_desc\">Patcher une application avec des modules intégrés.\\nL\\'application corrigée peut fonctionner sans le gestionnaire mais ne peut pas être gérée dynamiquement.\\nLes applications patchées intégrées peuvent être utilisées sur des appareils sur lesquels LSPatch Manager n\\'est pas installé.</string>\n    <string name=\"patch_embed_modules\">Intégrer des modules</string>\n    <string name=\"patch_debuggable\">Débogable</string>\n    <string name=\"patch_sigbypass\">Contournement des signatures</string>\n    <string name=\"patch_sigbypasslv0\">lv0 : Désactivé</string>\n    <string name=\"patch_sigbypasslv1\">lv1 : Contourner PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2 : Contourner PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Remplacer le code de version</string>\n    <string name=\"patch_override_version_code_desc\">Remplacer le code de version de l\\'application corrigée par 1\\nCela permet de rétrograder l\\'installation à l\\'avenir, et généralement cela n\\'affectera pas le code de version réellement perçu par l\\'application.</string>\n    <string name=\"patch_start\">Démarrer le correctif</string>\n    <string name=\"patch_return\">Retour</string>\n    <string name=\"patch_uninstall_text\">En raison de signatures différentes, vous devez désinstaller l\\'appli originale avant d\\'installer celle modifiée.\\nAssurez-vous d\\'avoir sauvegardé vos données personnelles.</string>\n    <string name=\"patch_install_successfully\">Installation réussie</string>\n    <string name=\"patch_install_failed\">Échec de l\\'installation</string>\n    <string name=\"patch_no_xposed_module\">Aucun module Xposed n\\'a été trouvé</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Sélectionner Applications</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Réglages</string>\n    <string name=\"settings_keystore\">Magasin de clés de signature</string>\n    <string name=\"settings_keystore_default\">Intégré</string>\n    <string name=\"settings_keystore_custom\">Personnalisé</string>\n    <string name=\"settings_keystore_dialog_title\">Magasin de clés personnalisé</string>\n    <string name=\"settings_keystore_file\">Fichier Keystore</string>\n    <string name=\"settings_keystore_password\">Mot de passe</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Mot de passe alias</string>\n    <string name=\"settings_keystore_desc\">Définir le keystore (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Mauvais type de keystore</string>\n    <string name=\"settings_keystore_wrong_password\">Mot de passe erroné pour le keystore</string>\n    <string name=\"settings_keystore_wrong_alias\">Nom d\\'alias erroné</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Mot de passe d\\'alias erroné</string>\n    <string name=\"settings_detail_patch_logs\">Détails des journaux du patch</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">जोड़ना</string>\n    <string name=\"install\">इंस्टॉल</string>\n    <string name=\"installing\">इंस्टॉल हो रहा है</string>\n    <string name=\"uninstall\">अनइंस्टॉल</string>\n    <string name=\"uninstalling\">अनइंस्टॉल हो रहा है</string>\n    <string name=\"copy_error\">कॉपी मैं त्रुटि है</string>\n    <string name=\"apps\">ऐप्स</string>\n    <string name=\"modules\">मॉड्यूलस</string>\n    <string name=\"shizuku_available\">शिज़ुकु सेवा उपलब्ध</string>\n    <string name=\"shizuku_unavailable\">शिज़ुकु सेवा कनेक्ट नहीं है</string>\n    <string name=\"screen_repo\">रेपो</string>\n    <string name=\"screen_logs\">लॉग्स</string>\n    <string name=\"off\">बंद</string>\n    <string name=\"error_unknown\"></string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">कुछ फ़ंक्शन अनुपलब्ध</string>\n    <string name=\"home_api_version\">एपीआई संस्करण</string>\n    <string name=\"home_lspatch_version\">एलएसपैच संस्करण</string>\n    <string name=\"home_framework_version\">फ्रेमवर्क संस्करण</string>\n    <string name=\"home_system_version\">सिस्टम संस्करण</string>\n    <string name=\"home_device\">उपकरण</string>\n    <string name=\"home_system_abi\">सिस्टम एबीआई</string>\n    <string name=\"home_info_copied\">क्लिपबोर्ड पर नकल</string>\n    <string name=\"home_support\">सहायता</string>\n    <string name=\"home_description\">LSPatch LSPosed कोर पर आधारित एक निःशुल्क गैर-रूट Xposed ढांचा है।</string>\n    <string name=\"home_view_source_code\"><![CDATA[%1$s<br/>पर सोर्स कोड देखें हमारे %2$s चैनल से जुड़ें]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">प्रबंधित करना</string>\n    <string name=\"manage_loading\">लोड हो रहा है</string>\n    <string name=\"manage_no_apps\">अभी तक कोई पैच किया गया ऐप्स नहीं</string>\n    <string name=\"manage_rolling\">रोलिंग</string>\n    <string name=\"manage_update_loader\">लोडर अपडेट करें</string>\n    <string name=\"manage_update_loader_successfully\">सफलतापूर्वक अपडेट करें</string>\n    <string name=\"manage_update_loader_failed\">अपडेट विफल हुआ</string>\n    <string name=\"manage_module_scope\">मॉड्यूल स्कोप</string>\n    <string name=\"manage_optimize\">अनुकूलन</string>\n    <string name=\"manage_optimize_successfully\">सफलतापूर्वक अनुकूलित करें</string>\n    <string name=\"manage_optimize_failed\">अनुकूलन विफल</string>\n    <string name=\"manage_uninstall_successfully\">सफलतापूर्वक अनइंस्टॉल करें</string>\n    <string name=\"manage_no_modules\">अभी तक कोई मॉड्यूल नहीं</string>\n    <string name=\"manage_module_settings\">मॉड्यूल सेटिंग्स</string>\n    <string name=\"manage_app_info\">अनुप्रयोग की जानकारी</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">नया पैच</string>\n    <string name=\"patch_select_dir_title\">भंडारण निर्देशिका का चयन करें</string>\n    <string name=\"patch_select_dir_text\">पैच किए गए एपीके को स्टोर करने के लिए एक निर्देशिका का चयन करें</string>\n    <string name=\"patch_select_dir_error\">संग्रहण निर्देशिका सेट करते समय त्रुटि</string>\n    <string name=\"patch_from_storage\">स्टोरेज से एपीके चुनें</string>\n    <string name=\"patch_from_applist\">एक इंस्टॉल किए गए ऐप का चयन करें</string>\n    <string name=\"patch_mode\">पैच मोड</string>\n    <string name=\"patch_local\">स्थानीय</string>\n    <string name=\"patch_local_desc\">बिना मॉड्यूल एम्बेडेड किसी ऐप को पैच करें।\\nएक्सपोज़ड स्कोप को री-पैच के बिना गतिशील रूप से बदला जा सकता है।\\nस्थानीय पैच किए गए ऐप्स केवल स्थानीय डिवाइस पर चल सकते हैं।</string>\n    <string name=\"patch_integrated\">एकीकृत</string>\n    <string name=\"patch_integrated_desc\">एम्बेडेड मॉड्यूल के साथ ऐप को पैच करें।\\nपैच किए गए ऐप प्रबंधक के बिना चल सकते हैं, लेकिन गतिशील रूप से प्रबंधित नहीं किए जा सकते।\\nएकीकृत पैच किए गए ऐप्स का उपयोग उन उपकरणों पर किया जा सकता है जिनमें LSPatch Manager स्थापित नहीं है।</string>\n    <string name=\"patch_embed_modules\">मॉड्यूल एम्बेड करें</string>\n    <string name=\"patch_debuggable\">डीबग करने योग्य</string>\n    <string name=\"patch_sigbypass\">सिग्नेचर बायपास</string>\n    <string name=\"patch_sigbypasslv0\">lv0: बंद</string>\n    <string name=\"patch_sigbypasslv1\">lv1: बाईपास पीएम</string>\n    <string name=\"patch_sigbypasslv2\">lv2: बाईपास PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">संस्करण कोड ओवरराइड करें</string>\n    <string name=\"patch_override_version_code_desc\">पैच किए गए ऐप के संस्करण कोड को 1\\nपर ओवरराइड करें यह भविष्य में डाउनग्रेड इंस्टॉलेशन की अनुमति देता है, और आम तौर पर यह एप्लिकेशन द्वारा वास्तव में देखे जाने वाले संस्करण कोड को प्रभावित नहीं करेगा।</string>\n    <string name=\"patch_start\">पैच शुरू करें</string>\n    <string name=\"patch_return\">वापस करना</string>\n    <string name=\"patch_uninstall_text\">अलग-अलग हस्ताक्षरों के कारण, आपको पैच किए गए ऐप को इंस्टॉल करने से पहले मूल ऐप को अनइंस्टॉल करना होगा।\\nसुनिश्चित करें कि आपने व्यक्तिगत डेटा का बैकअप लिया है।</string>\n    <string name=\"patch_install_successfully\">सफलतापूर्वक स्थापित करें</string>\n    <string name=\"patch_install_failed\">स्थापित करना विफल</string>\n    <string name=\"patch_no_xposed_module\"></string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">ऐप्स चुनें</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">समायोजन</string>\n    <string name=\"settings_keystore\">सिग्नेचर कीस्टोर</string>\n    <string name=\"settings_keystore_default\">में निर्मित</string>\n    <string name=\"settings_keystore_custom\">रिवाज़</string>\n    <string name=\"settings_keystore_dialog_title\">कस्टम कीस्टोर</string>\n    <string name=\"settings_keystore_file\">कीस्टोर फ़ाइल</string>\n    <string name=\"settings_keystore_password\">पासवर्ड</string>\n    <string name=\"settings_keystore_alias\">उपनाम</string>\n    <string name=\"settings_keystore_alias_password\">उपनाम पासवर्ड</string>\n    <string name=\"settings_keystore_desc\">कीस्टोर सेट करें (बीकेएस)</string>\n    <string name=\"settings_keystore_wrong_keystore\">गलत प्रकार का कीस्टोर</string>\n    <string name=\"settings_keystore_wrong_password\">गलत कीस्टोर पासवर्ड</string>\n    <string name=\"settings_keystore_wrong_alias\">गलत उपनाम</string>\n    <string name=\"settings_keystore_wrong_alias_password\">गलत उपनाम पासवर्ड</string>\n    <string name=\"settings_detail_patch_logs\">विस्तार पैच लॉग</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-hr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Dodaj</string>\n    <string name=\"install\">Instaliraj</string>\n    <string name=\"installing\">Instaliranje</string>\n    <string name=\"uninstall\">Deinstaliraj</string>\n    <string name=\"uninstalling\">Deinstaliranje</string>\n    <string name=\"copy_error\">Greška kopiranja</string>\n    <string name=\"apps\">Aplikacije</string>\n    <string name=\"modules\">Moduli</string>\n    <string name=\"shizuku_available\">Shizuku usluga dostupna</string>\n    <string name=\"shizuku_unavailable\">Usluga Shizuku nije povezana</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Dnevnici</string>\n    <string name=\"off\">Isključeno</string>\n    <string name=\"error_unknown\">Nepoznata pogreška</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Neke funkcije nisu dostupne</string>\n    <string name=\"home_api_version\">API verzija</string>\n    <string name=\"home_lspatch_version\">LSPatch verzija</string>\n    <string name=\"home_framework_version\">Framework verzija</string>\n    <string name=\"home_system_version\">Verzija sustava</string>\n    <string name=\"home_device\">Uređaj</string>\n    <string name=\"home_system_abi\">Sustav ABI</string>\n    <string name=\"home_info_copied\">Kopirano u međuspremnik</string>\n    <string name=\"home_support\">Podrška</string>\n    <string name=\"home_description\">LSPatch je besplatni Xposed framework bez root-a temeljen na LSPosed jezgri.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Pogledajte izvorni kod na %1$s<br/>Pridružite se našem %2$s kanalu]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Upravljanje</string>\n    <string name=\"manage_loading\">Učitavanje</string>\n    <string name=\"manage_no_apps\">Još nema zakrpanih aplikacija</string>\n    <string name=\"manage_rolling\">Kotrljanje</string>\n    <string name=\"manage_update_loader\">Ažuriraj učitavač</string>\n    <string name=\"manage_update_loader_successfully\">Uspješno ažurirano</string>\n    <string name=\"manage_update_loader_failed\">Ažuriranje nije uspjelo</string>\n    <string name=\"manage_module_scope\">Opseg modula</string>\n    <string name=\"manage_optimize\">Optimiziraj</string>\n    <string name=\"manage_optimize_successfully\">Optimiranje uspješno</string>\n    <string name=\"manage_optimize_failed\">Optimizacija nije uspjela</string>\n    <string name=\"manage_uninstall_successfully\">Deinstaliranje uspješno</string>\n    <string name=\"manage_no_modules\">Još nema modula</string>\n    <string name=\"manage_module_settings\">Postavke modula</string>\n    <string name=\"manage_app_info\">Informacije o aplikaciji</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nova zakrpa</string>\n    <string name=\"patch_select_dir_title\">Odaberite direktorij za pohranu</string>\n    <string name=\"patch_select_dir_text\">Odaberite direktorij za pohranu zakrpanih apk-ova</string>\n    <string name=\"patch_select_dir_error\">Pogreška prilikom postavljanja direktorija za pohranu</string>\n    <string name=\"patch_from_storage\">Odaberite apk(ove) iz pohrane</string>\n    <string name=\"patch_from_applist\">Odaberite instaliranu aplikaciju</string>\n    <string name=\"patch_mode\">Način zakrpe</string>\n    <string name=\"patch_local\">Lokalni</string>\n    <string name=\"patch_local_desc\">Zakrpite aplikaciju bez ugrađenih modula.\\nXposed opseg može se mijenjati dinamički bez ponovnog zakrpa.\\nLokalno zakrpane aplikacije mogu se izvoditi samo na lokalnom uređaju.</string>\n    <string name=\"patch_integrated\">Integriran</string>\n    <string name=\"patch_integrated_desc\">Zakrpite aplikaciju s ugrađenim modulima.\\nZakrpana aplikacija može raditi bez upravitelja, ali se njome ne može upravljati dinamički.\\nIntegrirane zakrpane aplikacije mogu se koristiti na uređajima koji nemaju instaliran LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Ugradite module</string>\n    <string name=\"patch_debuggable\">Mogućnost otklanjanja pogrešaka</string>\n    <string name=\"patch_sigbypass\">Premosnica potpisa</string>\n    <string name=\"patch_sigbypasslv0\">lv0: isključeno</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Zaobići PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Zaobići PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Nadjačaj kod verzije</string>\n    <string name=\"patch_override_version_code_desc\">Nadjačajte kod verzije zakrpane aplikacije na 1\\nTo omogućuje instalaciju na stariju verziju u budućnosti i općenito to neće utjecati na kod verzije koji aplikacija stvarno percipira</string>\n    <string name=\"patch_start\">Pokreni zakrpu</string>\n    <string name=\"patch_return\">Povratak</string>\n    <string name=\"patch_uninstall_text\">Zbog različitih potpisa morate deinstalirati izvornu aplikaciju prije instaliranja zakrpane.\\nProvjerite jeste li sigurnosno kopirali osobne podatke.</string>\n    <string name=\"patch_install_successfully\">Instalirajte uspješno</string>\n    <string name=\"patch_install_failed\">Instalacija nije uspjela</string>\n    <string name=\"patch_no_xposed_module\">Nisu pronađeni Xposed moduli</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Odaberite Aplikacije</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">postavke</string>\n    <string name=\"settings_keystore\">Spremište ključeva potpisa</string>\n    <string name=\"settings_keystore_default\">Ugrađeni</string>\n    <string name=\"settings_keystore_custom\">Prilagođen</string>\n    <string name=\"settings_keystore_dialog_title\">Prilagođeno spremište ključeva</string>\n    <string name=\"settings_keystore_file\">Datoteka spremišta ključeva</string>\n    <string name=\"settings_keystore_password\">Lozinka</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias lozinka</string>\n    <string name=\"settings_keystore_desc\">Postavi spremište ključeva (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Pogrešna vrsta spremišta ključeva</string>\n    <string name=\"settings_keystore_wrong_password\">Pogrešna lozinka spremišta ključeva</string>\n    <string name=\"settings_keystore_wrong_alias\">Pogrešno pseudonim</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Pogrešna lozinka za alias</string>\n    <string name=\"settings_detail_patch_logs\">Dnevnici zakrpa detalja</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-hu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Hozzáadás</string>\n    <string name=\"install\">Telepítés</string>\n    <string name=\"installing\">Telepítés folyamatban</string>\n    <string name=\"uninstall\">Eltávolítás</string>\n    <string name=\"uninstalling\">Eltávolítás folyamatban</string>\n    <string name=\"copy_error\">Másolási hiba</string>\n    <string name=\"apps\">Alkalmazások</string>\n    <string name=\"modules\">Modulok</string>\n    <string name=\"shizuku_available\">A Shizuku szolgáltatás elérhető</string>\n    <string name=\"shizuku_unavailable\">A Shizuku szolgáltatás nincs csatlakoztatva</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Naplók</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Ismeretlen hiba</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Néhány funkció nem elérhető</string>\n    <string name=\"home_api_version\">API verzió</string>\n    <string name=\"home_lspatch_version\">LSPatch verzió</string>\n    <string name=\"home_framework_version\">Keretrendszer verziója</string>\n    <string name=\"home_system_version\">Rendszer verzió</string>\n    <string name=\"home_device\">Eszköz</string>\n    <string name=\"home_system_abi\">Rendszer ABI</string>\n    <string name=\"home_info_copied\">Vágólapra másolva</string>\n    <string name=\"home_support\">Támogatás</string>\n    <string name=\"home_description\">Az LSPatch egy ingyenes, nem root Xposed keretrendszer, amely az LSPosed magon alapul.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Forráskód megtekintése itt: %1$s<br/>Csatlakozzon %2$s csatornánkhoz]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Kezelés</string>\n    <string name=\"manage_loading\">Betöltés</string>\n    <string name=\"manage_no_apps\">Még nincsenek patchelt alkalmazások</string>\n    <string name=\"manage_rolling\">Gördülő</string>\n    <string name=\"manage_update_loader\">Betöltő frissítése</string>\n    <string name=\"manage_update_loader_successfully\">A frissítés sikeres</string>\n    <string name=\"manage_update_loader_failed\">A frissítés sikertelen</string>\n    <string name=\"manage_module_scope\">A modul hatóköre</string>\n    <string name=\"manage_optimize\">Optimalizálás</string>\n    <string name=\"manage_optimize_successfully\">Az optimalizálás sikeres</string>\n    <string name=\"manage_optimize_failed\">Az optimalizálás nem sikerült</string>\n    <string name=\"manage_uninstall_successfully\">Az eltávolítás sikeres</string>\n    <string name=\"manage_no_modules\">Még nincsenek modulok</string>\n    <string name=\"manage_module_settings\">Modul beállítások</string>\n    <string name=\"manage_app_info\">App info</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Új Patch</string>\n    <string name=\"patch_select_dir_title\">Válassza ki a tárolási könyvtárat</string>\n    <string name=\"patch_select_dir_text\">Válasszon ki egy könyvtárat a patchelt apk-k tárolására</string>\n    <string name=\"patch_select_dir_error\">Hiba a tárolási könyvtár beállításakor</string>\n    <string name=\"patch_from_storage\">Válassza ki az apk-t a tárhelyről</string>\n    <string name=\"patch_from_applist\">Válasszon ki egy telepített alkalmazást</string>\n    <string name=\"patch_mode\">Patch mód</string>\n    <string name=\"patch_local\">Helyi</string>\n    <string name=\"patch_local_desc\">Beágyazott modulok nélküli alkalmazás foltozása.\\nAz Xposed hatókör dinamikusan megváltoztatható újrapatchelés nélkül.\\nA helyi javított alkalmazások csak a helyi eszközön futhatnak.</string>\n    <string name=\"patch_integrated\">Integrált</string>\n    <string name=\"patch_integrated_desc\">Patcheljen egy alkalmazást beágyazott modulokkal.\\nA patchelt alkalmazás futhat a Manager nélkül, de nem kezelhető dinamikusan.\\nAz integrált\\nhordozható, patchelt alkalmazások olyan eszközökön használhatók, amelyeken nincs telepítve az LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Modulok beágyazása</string>\n    <string name=\"patch_debuggable\">Hibakeresés</string>\n    <string name=\"patch_sigbypass\">Aláírás megkerülése</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Ki</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Bypass PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PM megkerülése + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Verziókód felülírása</string>\n    <string name=\"patch_override_version_code_desc\">A javított alkalmazás verziószámának felülírása 1-re\\nEz lehetővé teszi a jövőbeni downgrade telepítést, és általában ez nem befolyásolja az alkalmazás által ténylegesen érzékelt verziószámot.</string>\n    <string name=\"patch_start\">Indítsa el a Patch-et</string>\n    <string name=\"patch_return\">Visszatérés</string>\n    <string name=\"patch_uninstall_text\">Az eltérő aláírások miatt a javított alkalmazás telepítése előtt el kell távolítania az eredeti alkalmazást.\\nGyőződjön meg arról, hogy biztonsági másolatot készített a személyes adatokról.</string>\n    <string name=\"patch_install_successfully\">Sikeres telepítés</string>\n    <string name=\"patch_install_failed\">A telepítés sikertelen</string>\n    <string name=\"patch_no_xposed_module\">Nem találtak Xposed modul(ok)at</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Alkalmazások kiválasztása</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Beállítások</string>\n    <string name=\"settings_keystore\">Aláírás kulcstároló</string>\n    <string name=\"settings_keystore_default\">Beépített</string>\n    <string name=\"settings_keystore_custom\">Egyedi</string>\n    <string name=\"settings_keystore_dialog_title\">Egyéni kulcstár</string>\n    <string name=\"settings_keystore_file\">Kulcstároló fájl</string>\n    <string name=\"settings_keystore_password\">Jelszó</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias jelszó</string>\n    <string name=\"settings_keystore_desc\">Kulcstároló beállítása (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Rossz típusú kulcstároló</string>\n    <string name=\"settings_keystore_wrong_password\">Rossz kulcstároló jelszó</string>\n    <string name=\"settings_keystore_wrong_alias\">Rossz alias név</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Rossz alias jelszó</string>\n    <string name=\"settings_detail_patch_logs\">Részletes patch naplók</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Tambah</string>\n    <string name=\"install\">Install</string>\n    <string name=\"installing\">Menginstal</string>\n    <string name=\"uninstall\">Copot pemasangan</string>\n    <string name=\"uninstalling\">Mencopot pemasangan</string>\n    <string name=\"copy_error\">Kesalahan penyalinan</string>\n    <string name=\"apps\">Aplikasi</string>\n    <string name=\"modules\">Modul</string>\n    <string name=\"shizuku_available\">Layanan Shizuku tersedia</string>\n    <string name=\"shizuku_unavailable\">Layanan Shizuku tidak terhubung</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Log</string>\n    <string name=\"off\">Mati</string>\n    <string name=\"error_unknown\"></string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Beberapa fungsi tidak tersedia</string>\n    <string name=\"home_api_version\">Versi API</string>\n    <string name=\"home_lspatch_version\">Versi LSPatch</string>\n    <string name=\"home_framework_version\">Versi Kerangka</string>\n    <string name=\"home_system_version\">Versi Sistem</string>\n    <string name=\"home_device\">Perangkat</string>\n    <string name=\"home_system_abi\">Sistem ABI</string>\n    <string name=\"home_info_copied\">Disalin ke papan klip</string>\n    <string name=\"home_support\">Mendukung</string>\n    <string name=\"home_description\">LSPatch adalah kerangka kerja Xposed non-root gratis berdasarkan inti LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Lihat kode sumber di %1$s<br/>Bergabunglah dengan %2$s saluran kami]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Kelola</string>\n    <string name=\"manage_loading\">Memuat</string>\n    <string name=\"manage_no_apps\">Belum ada aplikasi yang ditambal</string>\n    <string name=\"manage_rolling\">Bergulir</string>\n    <string name=\"manage_update_loader\">Perbarui pemuat</string>\n    <string name=\"manage_update_loader_successfully\">Pembaruan berhasil</string>\n    <string name=\"manage_update_loader_failed\">Pembaruan gagal</string>\n    <string name=\"manage_module_scope\">Lingkup modul</string>\n    <string name=\"manage_optimize\">Optimalkan</string>\n    <string name=\"manage_optimize_successfully\">Optimalkan berhasil</string>\n    <string name=\"manage_optimize_failed\">Pengoptimalan gagal</string>\n    <string name=\"manage_uninstall_successfully\">Copot pemasangan dengan sukses</string>\n    <string name=\"manage_no_modules\">Belum ada modul</string>\n    <string name=\"manage_module_settings\">Pengaturan modul</string>\n    <string name=\"manage_app_info\">Info aplikasi</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Tambalan Baru</string>\n    <string name=\"patch_select_dir_title\">Pilih direktori penyimpanan</string>\n    <string name=\"patch_select_dir_text\">Pilih direktori untuk menyimpan apk yang ditambal</string>\n    <string name=\"patch_select_dir_error\">Kesalahan saat mengatur direktori penyimpanan</string>\n    <string name=\"patch_from_storage\">Pilih apk dari penyimpanan</string>\n    <string name=\"patch_from_applist\">Pilih aplikasi yang diinstal</string>\n    <string name=\"patch_mode\">Modus Patch</string>\n    <string name=\"patch_local\">Lokal</string>\n    <string name=\"patch_local_desc\">Menambal aplikasi tanpa modul yang tertanam.\\nCakupan Xposed dapat diubah secara dinamis tanpa menambal ulang.\\nAplikasi yang ditambal lokal hanya dapat berjalan di perangkat lokal.</string>\n    <string name=\"patch_integrated\">Terintegrasi</string>\n    <string name=\"patch_integrated_desc\">Patch aplikasi dengan modul yang disematkan.\\nAplikasi yang di-patch dapat berjalan tanpa manajer, tetapi tidak dapat dikelola secara dinamis.\\nAplikasi yang di-patch terintegrasi dapat digunakan pada perangkat yang tidak memasang LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Sematkan modul</string>\n    <string name=\"patch_debuggable\">Dapat di-debug</string>\n    <string name=\"patch_sigbypass\">Bypass tanda tangan</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Mati</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Bypass PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Ganti kode versi</string>\n    <string name=\"patch_override_version_code_desc\">Ganti kode versi aplikasi yang ditambal ke 1\\nIni memungkinkan penginstalan downgrade di masa mendatang, dan umumnya ini tidak akan memengaruhi kode versi yang sebenarnya dirasakan oleh aplikasi</string>\n    <string name=\"patch_start\">Mulai Patch</string>\n    <string name=\"patch_return\">Kembali</string>\n    <string name=\"patch_uninstall_text\">Dikarenakan adanya perbedaan tanda tangan, Anda perlu menghapus aplikasi asli sebelum menginstal aplikasi yang telah di-patch.\\nPastikan Anda telah mencadangkan data personal.</string>\n    <string name=\"patch_install_successfully\">Instal berhasil</string>\n    <string name=\"patch_install_failed\">Instal gagal</string>\n    <string name=\"patch_no_xposed_module\"></string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Pilih Aplikasi</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Pengaturan</string>\n    <string name=\"settings_keystore\">Toko kunci tanda tangan</string>\n    <string name=\"settings_keystore_default\">Built-in</string>\n    <string name=\"settings_keystore_custom\">Kebiasaan</string>\n    <string name=\"settings_keystore_dialog_title\">Toko kunci khusus</string>\n    <string name=\"settings_keystore_file\">File penyimpanan kunci</string>\n    <string name=\"settings_keystore_password\">Kata sandi</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Kata sandi alias</string>\n    <string name=\"settings_keystore_desc\">Setel penyimpanan kunci (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Jenis keystore yang salah</string>\n    <string name=\"settings_keystore_wrong_password\">Kata sandi keystore salah</string>\n    <string name=\"settings_keystore_wrong_alias\">Nama alias salah</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Kata sandi alias salah</string>\n    <string name=\"settings_detail_patch_logs\">Detail log tambalan</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Aggiungi</string>\n    <string name=\"install\">Installa</string>\n    <string name=\"installing\">Installazione</string>\n    <string name=\"uninstall\">Disinstalla</string>\n    <string name=\"uninstalling\">Disinstallazione</string>\n    <string name=\"copy_error\">Errore nella copia</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Moduli</string>\n    <string name=\"shizuku_available\">Servizio Shizuku disponibile</string>\n    <string name=\"shizuku_unavailable\">Servizio Shizuku non connesso</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Log</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Errore sconosciuto</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Alcune funzioni non sono disponibili</string>\n    <string name=\"home_api_version\">Versione API</string>\n    <string name=\"home_lspatch_version\">Versione LSPatch</string>\n    <string name=\"home_framework_version\">Versione del framework</string>\n    <string name=\"home_system_version\">Versione del sistema</string>\n    <string name=\"home_device\">Dispositivo</string>\n    <string name=\"home_system_abi\">ABI del sistema</string>\n    <string name=\"home_info_copied\">Copiato negli appunti</string>\n    <string name=\"home_support\">Supporto</string>\n    <string name=\"home_description\">LSPatch è un framework Xposed non-root gratuito basato sul core LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Visualizza il codice sorgente su %1$s<br/>Unisciti al nostro canale %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gestire</string>\n    <string name=\"manage_loading\">Caricamento in corso</string>\n    <string name=\"manage_no_apps\">Ancora nessuna app patchata</string>\n    <string name=\"manage_rolling\">Rotolamento</string>\n    <string name=\"manage_update_loader\">Aggiornamento del caricatore</string>\n    <string name=\"manage_update_loader_successfully\">Aggiornamento riuscito</string>\n    <string name=\"manage_update_loader_failed\">Aggiornamento fallito</string>\n    <string name=\"manage_module_scope\">Ambito del modulo</string>\n    <string name=\"manage_optimize\">Ottimizzare</string>\n    <string name=\"manage_optimize_successfully\">Ottimizzare con successo</string>\n    <string name=\"manage_optimize_failed\">Ottimizzare il fallimento</string>\n    <string name=\"manage_uninstall_successfully\">Disinstallazione riuscita</string>\n    <string name=\"manage_no_modules\">Non ci sono ancora moduli</string>\n    <string name=\"manage_module_settings\">Impostazioni del modulo</string>\n    <string name=\"manage_app_info\">Info sull\\'app</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nuova patch</string>\n    <string name=\"patch_select_dir_title\">Seleziona la directory di archiviazione</string>\n    <string name=\"patch_select_dir_text\">Seleziona una directory in cui archiviare gli apk patchati</string>\n    <string name=\"patch_select_dir_error\">Errore durante l\\'impostazione della directory di archiviazione</string>\n    <string name=\"patch_from_storage\">Seleziona gli apk dalla memoria</string>\n    <string name=\"patch_from_applist\">Seleziona un\\'app installata</string>\n    <string name=\"patch_mode\">Modalità patch</string>\n    <string name=\"patch_local\">Locale</string>\n    <string name=\"patch_local_desc\">Patchare un\\'applicazione senza moduli incorporati.\\nL\\'ambito di Xposed può essere modificato dinamicamente senza dover rifare la patch.\\nLe applicazioni locali patchate possono essere eseguite solo sul dispositivo locale.</string>\n    <string name=\"patch_integrated\">Integrato</string>\n    <string name=\"patch_integrated_desc\">Patcha un\\'app con moduli incorporati.\\nL\\'app patchata può essere eseguita senza il manager, ma non può essere gestita dinamicamente.\\nLe app patchate integrate possono essere utilizzate su dispositivi su cui non è installato LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Moduli incorporati</string>\n    <string name=\"patch_debuggable\">Debuggabile</string>\n    <string name=\"patch_sigbypass\">Bypass della firma</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Off</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Bypass PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Sostituisci il codice versione</string>\n    <string name=\"patch_override_version_code_desc\">Sostituisci il codice versione dell\\'applicazione patchata con 1\\nQuesto permette il downgrade dell\\'installazione in futuro, e generalmente questo non influenzerà il codice versione effettivamente percepito dall\\'applicazione</string>\n    <string name=\"patch_start\">Avvia Patch</string>\n    <string name=\"patch_return\">Ritorna</string>\n    <string name=\"patch_uninstall_text\">A causa delle diverse firme, è necessario disinstallare l\\'app originale prima di installare quella patchata.\\nAssicurati di aver eseguito il backup dei dati personali.</string>\n    <string name=\"patch_install_successfully\">Installato con successo</string>\n    <string name=\"patch_install_failed\">Installazione non riuscita</string>\n    <string name=\"patch_no_xposed_module\">Nessun modulo Xposed trovato</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Seleziona le app</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Impostazioni</string>\n    <string name=\"settings_keystore\">Firma del keystore</string>\n    <string name=\"settings_keystore_default\">Integrato</string>\n    <string name=\"settings_keystore_custom\">Personalizzato</string>\n    <string name=\"settings_keystore_dialog_title\">Keystore personalizzato</string>\n    <string name=\"settings_keystore_file\">File keystore</string>\n    <string name=\"settings_keystore_password\">Password</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Password alias</string>\n    <string name=\"settings_keystore_desc\">Imposta il keystore (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Tipo del keystore errato</string>\n    <string name=\"settings_keystore_wrong_password\">Password keystore errata</string>\n    <string name=\"settings_keystore_wrong_alias\">Nome alias errato</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Password alias errata</string>\n    <string name=\"settings_detail_patch_logs\">Registri dettagliati delle patch</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">לְהוֹסִיף</string>\n    <string name=\"install\">להתקין</string>\n    <string name=\"installing\">מתקין</string>\n    <string name=\"uninstall\">הסר את ההתקנה</string>\n    <string name=\"uninstalling\">מסיר התקנה</string>\n    <string name=\"copy_error\">שגיאת העתקה</string>\n    <string name=\"apps\">אפליקציות</string>\n    <string name=\"modules\">מודולים</string>\n    <string name=\"shizuku_available\">שירות שיזוקו זמין</string>\n    <string name=\"shizuku_unavailable\">שירות Shizuku לא מחובר</string>\n    <string name=\"screen_repo\">ריפו</string>\n    <string name=\"screen_logs\">יומנים</string>\n    <string name=\"off\">כבוי</string>\n    <string name=\"error_unknown\">שגיאה לא ידועה</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">חלק מהפונקציות אינן זמינות</string>\n    <string name=\"home_api_version\">גרסת API</string>\n    <string name=\"home_lspatch_version\">גרסת LSPatch</string>\n    <string name=\"home_framework_version\">גרסת מסגרת</string>\n    <string name=\"home_system_version\">גרסת מערכת</string>\n    <string name=\"home_device\">התקן</string>\n    <string name=\"home_system_abi\">מערכת ABI</string>\n    <string name=\"home_info_copied\">הועתק ללוח</string>\n    <string name=\"home_support\">תמיכה</string>\n    <string name=\"home_description\">LSPatch היא מסגרת חינמית ללא שורש Xposed המבוססת על ליבת LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[צפה בקוד המקור ב %1$s<br/>הצטרף לערוץ %2$s שלנו]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">לנהל</string>\n    <string name=\"manage_loading\">טוען</string>\n    <string name=\"manage_no_apps\">עדיין אין אפליקציות מתוקנות</string>\n    <string name=\"manage_rolling\">גִלגוּל</string>\n    <string name=\"manage_update_loader\">עדכון מטעין</string>\n    <string name=\"manage_update_loader_successfully\">עדכן בהצלחה</string>\n    <string name=\"manage_update_loader_failed\">עדכון נכשל</string>\n    <string name=\"manage_module_scope\">היקף מודול</string>\n    <string name=\"manage_optimize\">בצע אופטימיזציה</string>\n    <string name=\"manage_optimize_successfully\">בצע אופטימיזציה בהצלחה</string>\n    <string name=\"manage_optimize_failed\">האופטימיזציה נכשלה</string>\n    <string name=\"manage_uninstall_successfully\">הסר את ההתקנה בהצלחה</string>\n    <string name=\"manage_no_modules\">עדיין אין מודולים</string>\n    <string name=\"manage_module_settings\">הגדרות מודול</string>\n    <string name=\"manage_app_info\">מידע על האפליקציה</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">תיקון חדש</string>\n    <string name=\"patch_select_dir_title\">בחר ספריית אחסון</string>\n    <string name=\"patch_select_dir_text\">בחר ספרייה לאחסון ה-apks המתוקן</string>\n    <string name=\"patch_select_dir_error\">שגיאה בעת הגדרת ספריית אחסון</string>\n    <string name=\"patch_from_storage\">בחר apk(ים) מהאחסון</string>\n    <string name=\"patch_from_applist\">בחר אפליקציה מותקנת</string>\n    <string name=\"patch_mode\">מצב תיקון</string>\n    <string name=\"patch_local\">מְקוֹמִי</string>\n    <string name=\"patch_local_desc\">תקן אפליקציה ללא מודולים מוטבעים.\\nניתן לשנות את היקף Xposed באופן דינמי ללא תיקון מחדש.\\nאפליקציות מקומיות מתוקנות יכולות לפעול רק במכשיר המקומי.</string>\n    <string name=\"patch_integrated\">מְשׁוּלָב</string>\n    <string name=\"patch_integrated_desc\">תקן אפליקציה עם מודולים משובצים.\\nהאפליקציה המתוקנת יכולה לפעול ללא המנהל, אך לא ניתן לנהל אותה באופן דינמי.\\nניתן להשתמש באפליקציות מתוקנות משולבות במכשירים שלא מותקן בהם LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">הטמע מודולים</string>\n    <string name=\"patch_debuggable\">ניתן לניפוי באגים</string>\n    <string name=\"patch_sigbypass\">עוקף חתימה</string>\n    <string name=\"patch_sigbypasslv0\">lv0: כבוי</string>\n    <string name=\"patch_sigbypasslv1\">lv1: עוקף את PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: עוקף PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">עוקף את קוד הגרסה</string>\n    <string name=\"patch_override_version_code_desc\">עוקף את קוד הגרסה של האפליקציה המתוקנת ל-1\\nזה מאפשר שדרוג לאחור של התקנה בעתיד, ובדרך כלל זה לא ישפיע על קוד הגרסה הנתפס בפועל על ידי האפליקציה</string>\n    <string name=\"patch_start\">התחל תיקון</string>\n    <string name=\"patch_return\">לַחֲזוֹר</string>\n    <string name=\"patch_uninstall_text\">עקב חתימות שונות, עליך להסיר את ההתקנה של האפליקציה המקורית לפני התקנת האפליקציה המתוקנת.\\nודא שגיבית את הנתונים האישיים.</string>\n    <string name=\"patch_install_successfully\">התקן בהצלחה</string>\n    <string name=\"patch_install_failed\">ההתקנה נכשלה</string>\n    <string name=\"patch_no_xposed_module\">לא נמצאו מודולי Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">בחר אפליקציות</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">הגדרות</string>\n    <string name=\"settings_keystore\">מאגר מפתחות חתימה</string>\n    <string name=\"settings_keystore_default\">מובנה</string>\n    <string name=\"settings_keystore_custom\">המותאם אישית</string>\n    <string name=\"settings_keystore_dialog_title\">מאגר מפתחות מותאם אישית</string>\n    <string name=\"settings_keystore_file\">קובץ מאגר מפתחות</string>\n    <string name=\"settings_keystore_password\">סיסמה</string>\n    <string name=\"settings_keystore_alias\">כינוי</string>\n    <string name=\"settings_keystore_alias_password\">סיסמת כינוי</string>\n    <string name=\"settings_keystore_desc\">הגדר מאגר מפתחות (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">סוג שגוי של מאגר מפתחות</string>\n    <string name=\"settings_keystore_wrong_password\">סיסמת מאגר מפתחות שגויה</string>\n    <string name=\"settings_keystore_wrong_alias\">שם כינוי שגוי</string>\n    <string name=\"settings_keystore_wrong_alias_password\">סיסמת כינוי שגויה</string>\n    <string name=\"settings_detail_patch_logs\">פירוט יומני תיקון</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">追加</string>\n    <string name=\"install\">インストール</string>\n    <string name=\"installing\">インストール中</string>\n    <string name=\"uninstall\">アンインストール</string>\n    <string name=\"uninstalling\">アンインストール中</string>\n    <string name=\"copy_error\">エラーをコピー</string>\n    <string name=\"apps\">アプリ</string>\n    <string name=\"modules\">モジュール</string>\n    <string name=\"shizuku_available\">Shizukuが有効です</string>\n    <string name=\"shizuku_unavailable\">Shizukuが有効化されていません</string>\n    <string name=\"screen_repo\">リポジトリ</string>\n    <string name=\"screen_logs\">ログ</string>\n    <string name=\"off\">オフ</string>\n    <string name=\"error_unknown\">不明なエラー</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">一部の機能が使用できません</string>\n    <string name=\"home_api_version\">APIのバージョン</string>\n    <string name=\"home_lspatch_version\">LSPatchのバージョン</string>\n    <string name=\"home_framework_version\">フレームワークのバージョン</string>\n    <string name=\"home_system_version\">システムのバージョン</string>\n    <string name=\"home_device\">デバイス</string>\n    <string name=\"home_system_abi\">システムABI</string>\n    <string name=\"home_info_copied\">クリップボードにコピーしました</string>\n    <string name=\"home_support\">サポート</string>\n    <string name=\"home_description\">LSPatchは、LSPosedコアに基づく無料の非root環境向けXposedフレームワークです。</string>\n    <string name=\"home_view_source_code\"><![CDATA[%1$sでソースコードを見る<br/>%2$sチャンネルに参加する]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">管理</string>\n    <string name=\"manage_loading\">読み込み中</string>\n    <string name=\"manage_no_apps\">パッチが適用されたアプリはありません</string>\n    <string name=\"manage_rolling\">ロール</string>\n    <string name=\"manage_update_loader\">ローダーを更新</string>\n    <string name=\"manage_update_loader_successfully\">アップデートが正常に完了しました</string>\n    <string name=\"manage_update_loader_failed\">アップデートに失敗しました</string>\n    <string name=\"manage_module_scope\">モジュールのスコープ</string>\n    <string name=\"manage_optimize\">最適化</string>\n    <string name=\"manage_optimize_successfully\">正常に最適化されました</string>\n    <string name=\"manage_optimize_failed\">最適化に失敗しました</string>\n    <string name=\"manage_uninstall_successfully\">正常にアンインストールされました</string>\n    <string name=\"manage_no_modules\">モジュールはまだありません</string>\n    <string name=\"manage_module_settings\">モジュールの設定</string>\n    <string name=\"manage_app_info\">アプリの情報</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">新しいパッチ</string>\n    <string name=\"patch_select_dir_title\">ディレクトリの選択</string>\n    <string name=\"patch_select_dir_text\">パッチを適用したapkを保存するディレクトリを選択してください</string>\n    <string name=\"patch_select_dir_error\">ディレクトリの設定中にエラーが発生しました</string>\n    <string name=\"patch_from_storage\">ストレージからapkを選択</string>\n    <string name=\"patch_from_applist\">インストールされているアプリを選択</string>\n    <string name=\"patch_mode\">パッチモード</string>\n    <string name=\"patch_local\">ローカル</string>\n    <string name=\"patch_local_desc\">モジュールを埋め込んでいないアプリにパッチを当てる。\\nXposedのスコープは、再パッチなしで動的に変更できます。\\nローカルでパッチを適用したアプリは、ローカルデバイスでのみ実行できます。</string>\n    <string name=\"patch_integrated\">統合</string>\n    <string name=\"patch_integrated_desc\">モジュールが埋め込まれたアプリにパッチを適用します。\\nパッチ適用されたアプリはマネージャーなしで実行できますが、動的に管理することはできません。\\n統合されたパッチ適用済みアプリは、LSPatch Manager がインストールされていないデバイスでも使用できます。</string>\n    <string name=\"patch_embed_modules\">モジュールを埋め込む</string>\n    <string name=\"patch_debuggable\">デバッグを有効化</string>\n    <string name=\"patch_sigbypass\">署名のバイパス</string>\n    <string name=\"patch_sigbypasslv0\">lv0: オフ</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PMをバイパス</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PMをバイパス + openat (libc)</string>\n    <string name=\"patch_override_version_code\">バージョンコードを上書き</string>\n    <string name=\"patch_override_version_code_desc\">パッチを適用するアプリのバージョンコードを1に上書きします。\\nこれを行う事で将来的にダウングレードのインストールが可能になります。アプリが通常時に認識するバージョンコードには影響は与えません。</string>\n    <string name=\"patch_start\">パッチを開始</string>\n    <string name=\"patch_return\">戻る</string>\n    <string name=\"patch_uninstall_text\">署名が異なるため、パッチを適用したアプリをインストールする前に、元のアプリをアンインストールする必要があります。\\nデータは必要に応じてバックアップしてください。</string>\n    <string name=\"patch_install_successfully\">正常にインストールされました</string>\n    <string name=\"patch_install_failed\">インストールに失敗しました</string>\n    <string name=\"patch_no_xposed_module\">Xposedモジュールが見つかりませんでした</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">アプリを選択</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">設定</string>\n    <string name=\"settings_keystore\">署名のキーストア</string>\n    <string name=\"settings_keystore_default\">ビルトイン</string>\n    <string name=\"settings_keystore_custom\">カスタム</string>\n    <string name=\"settings_keystore_dialog_title\">カスタムキーストア</string>\n    <string name=\"settings_keystore_file\">キーストアファイル</string>\n    <string name=\"settings_keystore_password\">パスワード</string>\n    <string name=\"settings_keystore_alias\">エイリアス</string>\n    <string name=\"settings_keystore_alias_password\">エイリアスのパスワード</string>\n    <string name=\"settings_keystore_desc\">キーストア (BKS) の設定</string>\n    <string name=\"settings_keystore_wrong_keystore\">キーストアの種類が間違っています</string>\n    <string name=\"settings_keystore_wrong_password\">キーストアのパスワードが違います</string>\n    <string name=\"settings_keystore_wrong_alias\">エイリアス名が間違っています</string>\n    <string name=\"settings_keystore_wrong_alias_password\">エイリアスのパスワードが違います</string>\n    <string name=\"settings_detail_patch_logs\">詳細なパッチログ</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">추가</string>\n    <string name=\"install\">설치</string>\n    <string name=\"installing\">설치 중</string>\n    <string name=\"uninstall\">제거</string>\n    <string name=\"uninstalling\">제거 중</string>\n    <string name=\"copy_error\">복사 오류</string>\n    <string name=\"apps\">앱</string>\n    <string name=\"modules\">모듈</string>\n    <string name=\"shizuku_available\">시즈쿠 서비스 가능</string>\n    <string name=\"shizuku_unavailable\">시즈쿠 서비스가 연결되지 않았습니다</string>\n    <string name=\"screen_repo\">레포</string>\n    <string name=\"screen_logs\">로그</string>\n    <string name=\"off\">끄다</string>\n    <string name=\"error_unknown\">알 수 없는 오류</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">일부 기능을 사용할 수 없음</string>\n    <string name=\"home_api_version\">API 버전</string>\n    <string name=\"home_lspatch_version\">LSPatch 버전</string>\n    <string name=\"home_framework_version\">프레임워크 버전</string>\n    <string name=\"home_system_version\">시스템 버전</string>\n    <string name=\"home_device\">장치</string>\n    <string name=\"home_system_abi\">시스템 ABI</string>\n    <string name=\"home_info_copied\">클립보드에 복사됨</string>\n    <string name=\"home_support\">지원</string>\n    <string name=\"home_description\">LSPatch는 LSPosed 코어를 기반으로 하는 무료 비루트 Xposed 프레임워크입니다.</string>\n    <string name=\"home_view_source_code\"><![CDATA[%1$s<br/>에서 소스 코드 보기 %2$s 채널 가입]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">관리하다</string>\n    <string name=\"manage_loading\">로딩 중</string>\n    <string name=\"manage_no_apps\">아직 패치된 앱이 없습니다.</string>\n    <string name=\"manage_rolling\">구르는</string>\n    <string name=\"manage_update_loader\">로더 업데이트</string>\n    <string name=\"manage_update_loader_successfully\">업데이트 성공</string>\n    <string name=\"manage_update_loader_failed\">업데이트가 실패</string>\n    <string name=\"manage_module_scope\">모듈 범위</string>\n    <string name=\"manage_optimize\">최적화</string>\n    <string name=\"manage_optimize_successfully\">최적화 성공</string>\n    <string name=\"manage_optimize_failed\">최적화 실패</string>\n    <string name=\"manage_uninstall_successfully\">성공적으로 제거함</string>\n    <string name=\"manage_no_modules\">아직 모듈이 없습니다.</string>\n    <string name=\"manage_module_settings\">모듈 설정</string>\n    <string name=\"manage_app_info\">앱 정보</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">새로운 패치</string>\n    <string name=\"patch_select_dir_title\">저장 디렉토리 선택</string>\n    <string name=\"patch_select_dir_text\">패치된 APK를 저장할 디렉토리를 선택하세요.</string>\n    <string name=\"patch_select_dir_error\">저장 디렉토리를 설정하는 동안 오류가 발생했습니다.</string>\n    <string name=\"patch_from_storage\">저장소에서 APK 선택</string>\n    <string name=\"patch_from_applist\">설치된 앱 선택</string>\n    <string name=\"patch_mode\">패치 모드</string>\n    <string name=\"patch_local\">로컬</string>\n    <string name=\"patch_local_desc\">모듈이 임베드되지 않은 앱을 패치합니다.\\n재패치 없이 동적으로 노출 범위를 변경할 수 있습니다.\\n로컬로 패치된 앱은 로컬 디바이스에서만 실행할 수 있습니다.</string>\n    <string name=\"patch_integrated\">통합</string>\n    <string name=\"patch_integrated_desc\">모듈이 포함된 앱을 패치합니다.\\n패치된 앱은 관리자 없이 실행할 수 있지만 동적으로 관리할 수는 없습니다.\\n통합 패치 앱은 LSPatch Manager가 설치되지 않은 장치에서 사용할 수 있습니다.</string>\n    <string name=\"patch_embed_modules\">모듈 포함</string>\n    <string name=\"patch_debuggable\">디버깅 가능</string>\n    <string name=\"patch_sigbypass\">서명 우회</string>\n    <string name=\"patch_sigbypasslv0\">lv0: 꺼짐</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM 우회</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PM + openat(libc) 우회</string>\n    <string name=\"patch_override_version_code\">버전 코드 덮어씌우기</string>\n    <string name=\"patch_override_version_code_desc\">패치된 앱의 버전 코드를 1\\n으로 덮어씌웁니다. 이렇게 하면 향후 다운그레이드 설치를 할 수 있으며 일반적으로 애플리케이션에서 실제로 인식하는 버전 코드에는 영향을 미치지 않습니다.</string>\n    <string name=\"patch_start\">패치 시작</string>\n    <string name=\"patch_return\">되돌리기</string>\n    <string name=\"patch_uninstall_text\">서명이 다르기 때문에 패치된 앱을 설치하기 전에 기존 앱을 제거해야 합니다.\\n개인 데이터를 백업했는지 확인하십시오.</string>\n    <string name=\"patch_install_successfully\">성공적으로 설치함</string>\n    <string name=\"patch_install_failed\">설치 실패함</string>\n    <string name=\"patch_no_xposed_module\">X포지드 모듈을 찾을 수 없습니다.</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">앱 선택</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">설정</string>\n    <string name=\"settings_keystore\">서명 키 저장소</string>\n    <string name=\"settings_keystore_default\">내장</string>\n    <string name=\"settings_keystore_custom\">사용자 정의</string>\n    <string name=\"settings_keystore_dialog_title\">사용자 정의 키 저장소</string>\n    <string name=\"settings_keystore_file\">키 저장소 파일</string>\n    <string name=\"settings_keystore_password\">비밀번호</string>\n    <string name=\"settings_keystore_alias\">별칭</string>\n    <string name=\"settings_keystore_alias_password\">별칭 비밀번호</string>\n    <string name=\"settings_keystore_desc\">키 저장소 설정(BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">잘못된 유형의 키 저장소</string>\n    <string name=\"settings_keystore_wrong_password\">잘못된 키 저장소 비밀번호</string>\n    <string name=\"settings_keystore_wrong_alias\">잘못된 별칭 이름</string>\n    <string name=\"settings_keystore_wrong_alias_password\">잘못된 별칭 암호</string>\n    <string name=\"settings_detail_patch_logs\">세부 패치 로그</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ku/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Lêzêdekirin</string>\n    <string name=\"install\">Lêkirin</string>\n    <string name=\"installing\">Sazkirin</string>\n    <string name=\"uninstall\">Rakirin</string>\n    <string name=\"uninstalling\">Rakirin</string>\n    <string name=\"copy_error\">Çewtiya kopîkirinê</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Modules</string>\n    <string name=\"shizuku_available\">Karûbarê Shizuku heye</string>\n    <string name=\"shizuku_unavailable\">Karûbarê Shizuku ne girêdayî ye</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Logs</string>\n    <string name=\"off\">Ji</string>\n    <string name=\"error_unknown\">Çewtiya nenas</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Hin fonksiyon ne berdest in</string>\n    <string name=\"home_api_version\">Guhertoya API</string>\n    <string name=\"home_lspatch_version\">Guhertoya LSPatch</string>\n    <string name=\"home_framework_version\">Guhertoya Çarçoveyê</string>\n    <string name=\"home_system_version\">Versiyon ji System</string>\n    <string name=\"home_device\">Sazî</string>\n    <string name=\"home_system_abi\">Pergala ABI</string>\n    <string name=\"home_info_copied\">Li clipboardê hate kopî kirin</string>\n    <string name=\"home_support\">Alîkarî</string>\n    <string name=\"home_description\">LSPatch çarçoveyek Xposed-a ne-root-a belaş e ku li ser bingeha LSPosed-ê ye.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Koda çavkaniyê li %1$s<br/>bibînin Tevlî %2$s kanala me bibin]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Rêvebirin</string>\n    <string name=\"manage_loading\">Barkirin</string>\n    <string name=\"manage_no_apps\">Hê sepanên patched tune</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Barkerê nûve bikin</string>\n    <string name=\"manage_update_loader_successfully\">Rojanekirin bi serkeftî</string>\n    <string name=\"manage_update_loader_failed\">Nûvekirin têk çû</string>\n    <string name=\"manage_module_scope\">Qada modulê</string>\n    <string name=\"manage_optimize\">Optimize bikin</string>\n    <string name=\"manage_optimize_successfully\">Optimize bi serkeftî</string>\n    <string name=\"manage_optimize_failed\">Optimîzekirin têk çû</string>\n    <string name=\"manage_uninstall_successfully\">Rakirina bi serkeftî</string>\n    <string name=\"manage_no_modules\">Hîn modul tune</string>\n    <string name=\"manage_module_settings\">Mîhengên Modulê</string>\n    <string name=\"manage_app_info\">Agahdariya app</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Patch Nû</string>\n    <string name=\"patch_select_dir_title\">Peldanka hilanînê hilbijêrin</string>\n    <string name=\"patch_select_dir_text\">Ji bo hilanîna apkên patched pelrêçek hilbijêrin</string>\n    <string name=\"patch_select_dir_error\">Di sazkirina pelrêça hilanînê de çewtî</string>\n    <string name=\"patch_from_storage\">Ji hilanînê apk(ên) hilbijêrin</string>\n    <string name=\"patch_from_applist\">Serlêdanek sazkirî hilbijêrin</string>\n    <string name=\"patch_mode\">Mode Patch</string>\n    <string name=\"patch_local\">Herêmî</string>\n    <string name=\"patch_local_desc\">Serlêdanek bêyî modulên pêvekirî paqij bikin.\\nQada Xposed dikare bi dînamîk bêyî nûvekirin were guheztin.\\nSerlêdanên paçkirî yên herêmî tenê dikarin li ser cîhaza herêmî bixebitin.</string>\n    <string name=\"patch_integrated\">Integrated</string>\n    <string name=\"patch_integrated_desc\">Serlêdanek bi modulên pêvekirî veqetînin.\\nSerlêdana paçkirî dikare bêyî rêveberê bixebite, lê bi dînamîk nayê rêvebirin.\\nSerlêdanên pejirandî yên yekbûyî dikarin li ser cîhazên ku Rêvebirê LSPatch-ê sazkirî ne têne bikar anîn.</string>\n    <string name=\"patch_embed_modules\">Modulên tevde bikin</string>\n    <string name=\"patch_debuggable\">Debuggable</string>\n    <string name=\"patch_sigbypass\">Îmzeya bipass</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Off</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM derbas bike</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PM + vekirî (libc) derbas bike</string>\n    <string name=\"patch_override_version_code\">Koda guhertoyê bişopîne</string>\n    <string name=\"patch_override_version_code_desc\">Koda guhertoya sepana pejirandî biguhezîne 1\\nEv rê dide sazkirina dakêşanê di pêşerojê de, û bi gelemperî ev ê bandorê li koda guhertoya ku bi rastî ji hêla serîlêdanê ve tê fêm kirin neke.</string>\n    <string name=\"patch_start\">Patchê dest pê bikin</string>\n    <string name=\"patch_return\">Vegerr</string>\n    <string name=\"patch_uninstall_text\">Ji ber îmzeyên cihêreng, hûn hewce ne ku berî ku ya patched saz bikin sepana orîjînal rakin.\\nPiştrast bike ku we daneya kesane piştguh kiriye.</string>\n    <string name=\"patch_install_successfully\">Bi serkeftî saz bike</string>\n    <string name=\"patch_install_failed\">Sazkirin têk çû</string>\n    <string name=\"patch_no_xposed_module\">Modul(ên) Xposed nehatin dîtin</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Serlêdan hilbijêrin</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Settings</string>\n    <string name=\"settings_keystore\">keystore îmza</string>\n    <string name=\"settings_keystore_default\">Avakirin</string>\n    <string name=\"settings_keystore_custom\">Hûnbunî</string>\n    <string name=\"settings_keystore_dialog_title\">keystore Custom</string>\n    <string name=\"settings_keystore_file\">Pelê keystore</string>\n    <string name=\"settings_keystore_password\">Şîfre</string>\n    <string name=\"settings_keystore_alias\">Navê dizî</string>\n    <string name=\"settings_keystore_alias_password\">Nasnav şîfreya</string>\n    <string name=\"settings_keystore_desc\">Set keystore (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Cûreyek çewt a keystore</string>\n    <string name=\"settings_keystore_wrong_password\">Şîfreya keystore çewt</string>\n    <string name=\"settings_keystore_wrong_alias\">Navê nasnavê xelet</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Şîfreya nasnavê çewt</string>\n    <string name=\"settings_detail_patch_logs\">Detail patch têketin</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-lt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Papildyti</string>\n    <string name=\"install\">Įdiekite</string>\n    <string name=\"installing\">diegimas</string>\n    <string name=\"uninstall\">Pašalinti</string>\n    <string name=\"uninstalling\">pašalinimas</string>\n    <string name=\"copy_error\">Kopijavimo klaida</string>\n    <string name=\"apps\">Programėlės</string>\n    <string name=\"modules\">Moduliai</string>\n    <string name=\"shizuku_available\">Galimas Shizuku servisas</string>\n    <string name=\"shizuku_unavailable\">„Shizuku“ paslauga neprijungta</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Žurnalai</string>\n    <string name=\"off\">Išjungta</string>\n    <string name=\"error_unknown\">Nežinoma klaida</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Kai kurios funkcijos nepasiekiamos</string>\n    <string name=\"home_api_version\">API versija</string>\n    <string name=\"home_lspatch_version\">LSPatch versija</string>\n    <string name=\"home_framework_version\">Framework versija</string>\n    <string name=\"home_system_version\">Sistemos versija</string>\n    <string name=\"home_device\">Įrenginys</string>\n    <string name=\"home_system_abi\">Sistema ABI</string>\n    <string name=\"home_info_copied\">Nukopijuota į mainų sritį</string>\n    <string name=\"home_support\">Palaikymas</string>\n    <string name=\"home_description\">LSPatch yra nemokama ne šakninė Xposed sistema, pagrįsta LSPosed branduoliu.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Peržiūrėkite šaltinio kodą %1$s<br/>Prisijunkite prie mūsų %2$s kanalo]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Tvarkykite</string>\n    <string name=\"manage_loading\">Įkeliama</string>\n    <string name=\"manage_no_apps\">Dar nėra pataisytų programų</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Atnaujinti pakrovėją</string>\n    <string name=\"manage_update_loader_successfully\">Sėkmingai atnaujinta</string>\n    <string name=\"manage_update_loader_failed\">Atnaujinti nepavyko</string>\n    <string name=\"manage_module_scope\">Modulio taikymo sritis</string>\n    <string name=\"manage_optimize\">Optimizuokite</string>\n    <string name=\"manage_optimize_successfully\">Sėkmingai optimizuoti</string>\n    <string name=\"manage_optimize_failed\">Optimizuoti nepavyko</string>\n    <string name=\"manage_uninstall_successfully\">Sėkmingai pašalinti</string>\n    <string name=\"manage_no_modules\">Modulių dar nėra</string>\n    <string name=\"manage_module_settings\">Modulio nustatymai</string>\n    <string name=\"manage_app_info\">Programėlės informacija</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Naujas pataisymas</string>\n    <string name=\"patch_select_dir_title\">Pasirinkite saugojimo katalogą</string>\n    <string name=\"patch_select_dir_text\">Pasirinkite katalogą, kuriame bus saugomi pataisyti APK</string>\n    <string name=\"patch_select_dir_error\">Klaida nustatant saugojimo katalogą</string>\n    <string name=\"patch_from_storage\">Pasirinkite apk (-us) iš saugyklos</string>\n    <string name=\"patch_from_applist\">Pasirinkite įdiegtą programą</string>\n    <string name=\"patch_mode\">Patch režimas</string>\n    <string name=\"patch_local\">Vietinis</string>\n    <string name=\"patch_local_desc\">Pataisykite programą be įterptųjų modulių.\\n\\\"Xposed\\\" apimtį galima dinamiškai keisti be pakartotinio pataisymo.\\nVietinės pataisytos programos gali veikti tik vietiniame įrenginyje.</string>\n    <string name=\"patch_integrated\">Integruota</string>\n    <string name=\"patch_integrated_desc\">Pataisykite programą su įterptais moduliais.\\nPataisyta programa gali veikti be tvarkyklės, bet negali būti valdoma dinamiškai.\\nIntegruotas pataisytas programas galima naudoti įrenginiuose, kuriuose nėra įdiegta LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Įterpti modulius</string>\n    <string name=\"patch_debuggable\">Derinamas</string>\n    <string name=\"patch_sigbypass\">Parašo apėjimas</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Išjungta</string>\n    <string name=\"patch_sigbypasslv1\">lv1: apeiti PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: apeiti PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Nepaisyti versijos kodo</string>\n    <string name=\"patch_override_version_code_desc\">Pakeiskite pataisytos programos versijos kodą į 1\\nTai leidžia ateityje sumažinti įdiegimo lygį, ir paprastai tai neturės įtakos programos faktiškai suvokiamam versijos kodui.</string>\n    <string name=\"patch_start\">Pradėti pataisą</string>\n    <string name=\"patch_return\">Grįžti</string>\n    <string name=\"patch_uninstall_text\">Dėl skirtingų parašų prieš įdiegdami pataisytą programą turite pašalinti originalią programą.\\nĮsitikinkite, kad turite atsargines asmens duomenų kopijas.</string>\n    <string name=\"patch_install_successfully\">Įdiegti sėkmingai</string>\n    <string name=\"patch_install_failed\">Diegimas nepavyko</string>\n    <string name=\"patch_no_xposed_module\">Nerastas (-i) nė vienas (-i) \\\"Xposed\\\" modulis (-iai)</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Pasirinkite programas</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Nustatymai</string>\n    <string name=\"settings_keystore\">Parašo raktų saugykla</string>\n    <string name=\"settings_keystore_default\">Įmontuotas</string>\n    <string name=\"settings_keystore_custom\">Pasirinktinis</string>\n    <string name=\"settings_keystore_dialog_title\">Pasirinktinė raktų saugykla</string>\n    <string name=\"settings_keystore_file\">Raktų saugyklos failas</string>\n    <string name=\"settings_keystore_password\">Slaptažodis</string>\n    <string name=\"settings_keystore_alias\">Pseudonimas</string>\n    <string name=\"settings_keystore_alias_password\">Pseudonimas slaptažodis</string>\n    <string name=\"settings_keystore_desc\">Nustatyti raktų saugyklą (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Neteisingas raktų saugyklos tipas</string>\n    <string name=\"settings_keystore_wrong_password\">Neteisingas raktų saugyklos slaptažodis</string>\n    <string name=\"settings_keystore_wrong_alias\">Neteisingas pseudonimas</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Neteisingas slapyvardžio slaptažodis</string>\n    <string name=\"settings_detail_patch_logs\">Išsamūs pataisų žurnalai</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Toevoegen</string>\n    <string name=\"install\">Installeer</string>\n    <string name=\"installing\">Installatie van</string>\n    <string name=\"uninstall\">Verwijder</string>\n    <string name=\"uninstalling\">De-installeren van</string>\n    <string name=\"copy_error\">Kopieerfout</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Modules</string>\n    <string name=\"shizuku_available\">Shizuku-service beschikbaar</string>\n    <string name=\"shizuku_unavailable\">Shizuku-service niet verbonden</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Logs</string>\n    <string name=\"off\">Uit</string>\n    <string name=\"error_unknown\">Onbekende fout</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Sommige functies niet beschikbaar</string>\n    <string name=\"home_api_version\">API-versie</string>\n    <string name=\"home_lspatch_version\">LSPatch-versie</string>\n    <string name=\"home_framework_version\">Framework-versie</string>\n    <string name=\"home_system_version\">Systeemversie</string>\n    <string name=\"home_device\">Apparaat</string>\n    <string name=\"home_system_abi\">Systeem ABI</string>\n    <string name=\"home_info_copied\">Gekopieerd naar het klembord</string>\n    <string name=\"home_support\">Steun</string>\n    <string name=\"home_description\">LSPatch is een gratis niet-root Xposed-framework op basis van LSPosed-kern.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Bekijk de broncode op %1$s<br/>Word lid van ons %2$s -kanaal]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Beheer</string>\n    <string name=\"manage_loading\">Bezig met laden</string>\n    <string name=\"manage_no_apps\">Nog geen gepatchte apps</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Update lader</string>\n    <string name=\"manage_update_loader_successfully\">Update met succes</string>\n    <string name=\"manage_update_loader_failed\">Update mislukt</string>\n    <string name=\"manage_module_scope\">Toepassingsgebied van de module</string>\n    <string name=\"manage_optimize\">Optimaliseren</string>\n    <string name=\"manage_optimize_successfully\">Succesvol optimaliseren</string>\n    <string name=\"manage_optimize_failed\">Optimaliseren mislukt</string>\n    <string name=\"manage_uninstall_successfully\">Installatie ongedaan maken</string>\n    <string name=\"manage_no_modules\">Nog geen modules</string>\n    <string name=\"manage_module_settings\">Module-instellingen</string>\n    <string name=\"manage_app_info\">App info</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nieuwe patch</string>\n    <string name=\"patch_select_dir_title\">Selecteer opslagmap</string>\n    <string name=\"patch_select_dir_text\">Selecteer een map om de gepatchte apks op te slaan</string>\n    <string name=\"patch_select_dir_error\">Fout bij het instellen van de opslagmap</string>\n    <string name=\"patch_from_storage\">Selecteer apk(s) uit opslag</string>\n    <string name=\"patch_from_applist\">Selecteer een geïnstalleerde app</string>\n    <string name=\"patch_mode\">Patch-modus</string>\n    <string name=\"patch_local\">lokaal</string>\n    <string name=\"patch_local_desc\">Een app patchen zonder geïntegreerde modules.\\nXposed scope kan dynamisch worden gewijzigd zonder opnieuw te patchen.\\nLokaal gepatchte apps kunnen alleen draaien op het lokale apparaat.</string>\n    <string name=\"patch_integrated\">Geïntegreerd</string>\n    <string name=\"patch_integrated_desc\">Patch een app met ingesloten modules.\\nDe gepatchte app kan zonder de manager worden uitgevoerd, maar kan niet dynamisch worden beheerd.\\nGeïntegreerde gepatchte apps kunnen worden gebruikt op apparaten waarop LSPatch Manager niet is geïnstalleerd.</string>\n    <string name=\"patch_embed_modules\">Modules insluiten</string>\n    <string name=\"patch_debuggable\">Foutopsporing</string>\n    <string name=\"patch_sigbypass\">Handtekening bypass</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Uit</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM overslaan</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Versiecode overschrijven</string>\n    <string name=\"patch_override_version_code_desc\">Overschrijf de versiecode van de gepatchte app naar 1\\nDit maakt downgrade installatie in de toekomst mogelijk, en over het algemeen zal dit geen invloed hebben op de versiecode die daadwerkelijk wordt waargenomen door de applicatie</string>\n    <string name=\"patch_start\">Patch starten</string>\n    <string name=\"patch_return\">Opbrengst</string>\n    <string name=\"patch_uninstall_text\">Vanwege verschillende handtekeningen moet u de originele app verwijderen voordat u de gepatchte app installeert.\\nZorg ervoor dat u een back-up hebt gemaakt van persoonlijke gegevens.</string>\n    <string name=\"patch_install_successfully\">Succesvol installeren</string>\n    <string name=\"patch_install_failed\">Installatie mislukt</string>\n    <string name=\"patch_no_xposed_module\">Geen Xposed-module(s) gevonden</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Selecteer toepassingen</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Instellingen</string>\n    <string name=\"settings_keystore\">Handtekening sleutelbewaarplaats</string>\n    <string name=\"settings_keystore_default\">Ingebouwd</string>\n    <string name=\"settings_keystore_custom\">Custom</string>\n    <string name=\"settings_keystore_dialog_title\">Aangepaste sleutelbewaarplaats</string>\n    <string name=\"settings_keystore_file\">Keystore bestand</string>\n    <string name=\"settings_keystore_password\">Wachtwoord</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias wachtwoord</string>\n    <string name=\"settings_keystore_desc\">Sleutelbewaarplaats instellen (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Verkeerde type sleutelbewaarplaats</string>\n    <string name=\"settings_keystore_wrong_password\">Verkeerd wachtwoord voor sleutelbewaarplaats</string>\n    <string name=\"settings_keystore_wrong_alias\">Verkeerde aliasnaam</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Verkeerd alias wachtwoord</string>\n    <string name=\"settings_detail_patch_logs\">Detail patch logs</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-no/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Legge til</string>\n    <string name=\"install\">Installere</string>\n    <string name=\"installing\">Installerer</string>\n    <string name=\"uninstall\">Avinstaller</string>\n    <string name=\"uninstalling\">Avinstallerer</string>\n    <string name=\"copy_error\">Kopifeil</string>\n    <string name=\"apps\">Apper</string>\n    <string name=\"modules\">Moduler</string>\n    <string name=\"shizuku_available\">Shizuku-tjeneste tilgjengelig</string>\n    <string name=\"shizuku_unavailable\">Shizuku-tjenesten er ikke tilkoblet</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Tømmerstokker</string>\n    <string name=\"off\">Av</string>\n    <string name=\"error_unknown\">Ukjent feil</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Noen funksjoner er utilgjengelige</string>\n    <string name=\"home_api_version\">API-versjon</string>\n    <string name=\"home_lspatch_version\">LSPatch-versjon</string>\n    <string name=\"home_framework_version\">Rammeversjon</string>\n    <string name=\"home_system_version\">Systemversjon</string>\n    <string name=\"home_device\">Enhet</string>\n    <string name=\"home_system_abi\">System ABI</string>\n    <string name=\"home_info_copied\">Kopiert til utklippstavlen</string>\n    <string name=\"home_support\">Brukerstøtte</string>\n    <string name=\"home_description\">LSPatch er et gratis ikke-root Xposed-rammeverk basert på LSPosed-kjerne.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Se kildekoden på %1$s<br/>Bli med i vår %2$s -kanal]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Få til</string>\n    <string name=\"manage_loading\">Laster</string>\n    <string name=\"manage_no_apps\">Ingen lappede apper ennå</string>\n    <string name=\"manage_rolling\">Rullende</string>\n    <string name=\"manage_update_loader\">Oppdater loader</string>\n    <string name=\"manage_update_loader_successfully\">Oppdatering vellykket</string>\n    <string name=\"manage_update_loader_failed\">Oppdatering mislyktes</string>\n    <string name=\"manage_module_scope\">Modulomfang</string>\n    <string name=\"manage_optimize\">Optimaliser</string>\n    <string name=\"manage_optimize_successfully\">Optimaliser vellykket</string>\n    <string name=\"manage_optimize_failed\">Optimaliseringen mislyktes</string>\n    <string name=\"manage_uninstall_successfully\">Avinstaller vellykket</string>\n    <string name=\"manage_no_modules\">Ingen moduler ennå</string>\n    <string name=\"manage_module_settings\">Modulinnstillinger</string>\n    <string name=\"manage_app_info\">App info</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Ny oppdatering</string>\n    <string name=\"patch_select_dir_title\">Velg lagringskatalog</string>\n    <string name=\"patch_select_dir_text\">Velg en katalog for å lagre de lappede APK-ene</string>\n    <string name=\"patch_select_dir_error\">Feil ved innstilling av lagringskatalog</string>\n    <string name=\"patch_from_storage\">Velg apk(er) fra lagring</string>\n    <string name=\"patch_from_applist\">Velg en installert app</string>\n    <string name=\"patch_mode\">Patch-modus</string>\n    <string name=\"patch_local\">Lokalt</string>\n    <string name=\"patch_local_desc\">Patch en app uten innebygde moduler.\\nXposed scope kan endres dynamisk uten re-patch.\\nLokale lappede apper kan bare kjøres på den lokale enheten.</string>\n    <string name=\"patch_integrated\">Integrert</string>\n    <string name=\"patch_integrated_desc\">Patch en app med innebygde moduler.\\nDen lappede appen kan kjøres uten administratoren, men kan ikke administreres dynamisk.\\nIntegrerte lappede apper kan brukes på enheter som ikke har LSPatch Manager installert.</string>\n    <string name=\"patch_embed_modules\">Bygg inn moduler</string>\n    <string name=\"patch_debuggable\">Kan feilsøkes</string>\n    <string name=\"patch_sigbypass\">Signaturomkjøring</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Av</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Omgå PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Overstyr versjonskoden</string>\n    <string name=\"patch_override_version_code_desc\">Overstyr den lappede appens versjonskode til 1\\nDette tillater nedgraderingsinstallasjon i fremtiden, og generelt vil dette ikke påvirke versjonskoden som faktisk oppfattes av applikasjonen</string>\n    <string name=\"patch_start\">Start patch</string>\n    <string name=\"patch_return\">Komme tilbake</string>\n    <string name=\"patch_uninstall_text\">På grunn av forskjellige signaturer, må du avinstallere den originale appen før du installerer den lappede.\\nSørg for at du har sikkerhetskopiert personlige data.</string>\n    <string name=\"patch_install_successfully\">Installer vellykket</string>\n    <string name=\"patch_install_failed\">Installasjonen mislyktes</string>\n    <string name=\"patch_no_xposed_module\">Ingen Xposed-modul(er) ble funnet</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Velg Apper</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Innstillinger</string>\n    <string name=\"settings_keystore\">Signatur nøkkellager</string>\n    <string name=\"settings_keystore_default\">Innebygd</string>\n    <string name=\"settings_keystore_custom\">Tilpasset</string>\n    <string name=\"settings_keystore_dialog_title\">Egendefinert nøkkellager</string>\n    <string name=\"settings_keystore_file\">Keystore-fil</string>\n    <string name=\"settings_keystore_password\">Passord</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias passord</string>\n    <string name=\"settings_keystore_desc\">Angi nøkkellager (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Feil type nøkkellager</string>\n    <string name=\"settings_keystore_wrong_password\">Feil nøkkellagerpassord</string>\n    <string name=\"settings_keystore_wrong_alias\">Feil aliasnavn</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Feil alias passord</string>\n    <string name=\"settings_detail_patch_logs\">Detalj patchlogger</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Dodaj</string>\n    <string name=\"install\">Zainstaluj</string>\n    <string name=\"installing\">Instalowanie</string>\n    <string name=\"uninstall\">Odinstaluj</string>\n    <string name=\"uninstalling\">Odinstalowywanie</string>\n    <string name=\"copy_error\">Błąd kopiowania</string>\n    <string name=\"apps\">Aplikacje</string>\n    <string name=\"modules\">Moduły</string>\n    <string name=\"shizuku_available\">Dostępna usługa Shizuku</string>\n    <string name=\"shizuku_unavailable\">Usługa Shizuku nie jest połączona</string>\n    <string name=\"screen_repo\">Repozytorium</string>\n    <string name=\"screen_logs\">Logi</string>\n    <string name=\"off\">Wyłącz</string>\n    <string name=\"error_unknown\">Nieznany błąd</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Niektóre funkcje niedostępne</string>\n    <string name=\"home_api_version\">Wersja API</string>\n    <string name=\"home_lspatch_version\">Wersja LSPatch</string>\n    <string name=\"home_framework_version\">Wersja Frameworka</string>\n    <string name=\"home_system_version\">Wersja systemu</string>\n    <string name=\"home_device\">Urządzenie</string>\n    <string name=\"home_system_abi\">ABI systemu</string>\n    <string name=\"home_info_copied\">Skopiowane do schowka</string>\n    <string name=\"home_support\">Wsparcie</string>\n    <string name=\"home_description\">LSPatch jest darmowym odłamem non-root Xposed framework bazowanym na rdzeniu LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Zobacz kod źródłowy na %1$s<br/>Dołącz do naszego kanału %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Zarządzaj</string>\n    <string name=\"manage_loading\">Ładowanie</string>\n    <string name=\"manage_no_apps\">Brak spatchowanych aplikacji</string>\n    <string name=\"manage_rolling\">Rolling</string>\n    <string name=\"manage_update_loader\">Zaktualizuj program ładujący</string>\n    <string name=\"manage_update_loader_successfully\">Zaktualizowano pomyślnie</string>\n    <string name=\"manage_update_loader_failed\">Aktualizacja nie powiodła się</string>\n    <string name=\"manage_module_scope\">Zakres modułu</string>\n    <string name=\"manage_optimize\">Optymalizuj</string>\n    <string name=\"manage_optimize_successfully\">Zoptymalizowano pomyślnie</string>\n    <string name=\"manage_optimize_failed\">Optymalizacja nie powiodła się</string>\n    <string name=\"manage_uninstall_successfully\">Odinstalowywanie pomyślne</string>\n    <string name=\"manage_no_modules\">Brak modułów</string>\n    <string name=\"manage_module_settings\">Ustawienia modułu</string>\n    <string name=\"manage_app_info\">Informacje o aplikacji</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nowy Patch</string>\n    <string name=\"patch_select_dir_title\">Wybierz katalog przechowywania</string>\n    <string name=\"patch_select_dir_text\">Wybierz katalog, w którym chcesz przechowywać spatchowane pliki apk</string>\n    <string name=\"patch_select_dir_error\">Błąd podczas ustawiania katalogu przechowywania</string>\n    <string name=\"patch_from_storage\">Wybierz plik(i) apk z pamięci</string>\n    <string name=\"patch_from_applist\">Wybierz zainstalowaną aplikację</string>\n    <string name=\"patch_mode\">Tryb Patchu</string>\n    <string name=\"patch_local\">Lokalny</string>\n    <string name=\"patch_local_desc\">Patchowanie aplikacji bez osadzonych modułów.\\nZakres Xposed może być dynamicznie zmieniany bez konieczności ponownego łatania.\\nLokalnie załatane aplikacje mogą działać tylko na lokalnym urządzeniu.</string>\n    <string name=\"patch_integrated\">Zintegrowane</string>\n    <string name=\"patch_integrated_desc\">Patchuj aplikację z osadzonymi modułami.\\nSpatchowana aplikacja może działać bez menedżera, ale nie mogą być one dynamicznie zarządzane.\\nZintegrowane spatchowane aplikacje mogą być użyte na urządzeniach, które nie mają zainstalowanego menedżera LSPosed.</string>\n    <string name=\"patch_embed_modules\">Osadź moduły</string>\n    <string name=\"patch_debuggable\">Możliwość debugowania</string>\n    <string name=\"patch_sigbypass\">Ominięcie podpisu</string>\n    <string name=\"patch_sigbypasslv0\">poziom 0: wyłączony</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Pomiń PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Pomiń PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Zastąp kod wersji</string>\n    <string name=\"patch_override_version_code_desc\">Zastąp kod wersji załatanej aplikacji na 1\\nUmożliwi to instalację w przyszłości, a generalnie nie będzie miało wpływu na kod wersji faktycznie odbierany przez aplikację</string>\n    <string name=\"patch_start\">Rozpocznij łatkę</string>\n    <string name=\"patch_return\">Powrót</string>\n    <string name=\"patch_uninstall_text\">Ze względu na różne sygnatury musisz odinstalować oryginalną aplikację przed zainstalowaniem poprawionej.\\nUpewnij się, że wykonałeś kopię zapasową danych osobowych.</string>\n    <string name=\"patch_install_successfully\">Zainstaluj pomyślnie</string>\n    <string name=\"patch_install_failed\">Instalacja nie powiodła się</string>\n    <string name=\"patch_no_xposed_module\">Nie znaleziono modułów Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Wybierz aplikacje</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Ustawienia</string>\n    <string name=\"settings_keystore\">Podpisz bazę kluczy</string>\n    <string name=\"settings_keystore_default\">Wbudowana strona</string>\n    <string name=\"settings_keystore_custom\">Własna strona</string>\n    <string name=\"settings_keystore_dialog_title\">Własny magazyn kluczy</string>\n    <string name=\"settings_keystore_file\">Plik Keystore</string>\n    <string name=\"settings_keystore_password\">Hasło</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias password</string>\n    <string name=\"settings_keystore_desc\">Ustawienie magazynu kluczy (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Nieprawidłowy typ magazynu kluczy</string>\n    <string name=\"settings_keystore_wrong_password\">Nieprawidłowe hasło do magazynu kluczy</string>\n    <string name=\"settings_keystore_wrong_alias\">Nieprawidłowa nazwa aliasu</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Nieprawidłowe hasło aliasu</string>\n    <string name=\"settings_detail_patch_logs\">Szczegóły logów poprawek</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Adicionar</string>\n    <string name=\"install\">Instalar</string>\n    <string name=\"installing\">Instalando</string>\n    <string name=\"uninstall\">Desinstalaroi</string>\n    <string name=\"uninstalling\">Desinstalarou</string>\n    <string name=\"copy_error\">Erro de cópia</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Módulos</string>\n    <string name=\"shizuku_available\">Serviço Shizuku disponível</string>\n    <string name=\"shizuku_unavailable\">Serviço Shizuku não conectado</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Registos</string>\n    <string name=\"off\">Desligado</string>\n    <string name=\"error_unknown\">Erro desconhecido</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Algumas funções indisponíveis</string>\n    <string name=\"home_api_version\">Versão da API</string>\n    <string name=\"home_lspatch_version\">Versão LSPatch</string>\n    <string name=\"home_framework_version\">Versão da estrutura</string>\n    <string name=\"home_system_version\">Versão do sistema</string>\n    <string name=\"home_device\">Dispositivo</string>\n    <string name=\"home_system_abi\">Sistema ABI</string>\n    <string name=\"home_info_copied\">Copiado para a área de transferência</string>\n    <string name=\"home_support\">Apoiar</string>\n    <string name=\"home_description\">LSPatch é uma estrutura Xposed não raiz gratuita baseada no núcleo LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Veja o código fonte em %1$s<br/>Junte-se ao nosso %2$s canais]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gerir</string>\n    <string name=\"manage_loading\">Carregando</string>\n    <string name=\"manage_no_apps\">Nenhum aplicativo corrigido ainda</string>\n    <string name=\"manage_rolling\">Rolagem</string>\n    <string name=\"manage_update_loader\">Carregador de actualização</string>\n    <string name=\"manage_update_loader_successfully\">Actualização com sucesso</string>\n    <string name=\"manage_update_loader_failed\">Actualização falhada</string>\n    <string name=\"manage_module_scope\">Âmbito do módulo</string>\n    <string name=\"manage_optimize\">Optimizar</string>\n    <string name=\"manage_optimize_successfully\">Optimizar com sucesso</string>\n    <string name=\"manage_optimize_failed\">Optimizar falhou</string>\n    <string name=\"manage_uninstall_successfully\">Desinstalar com sucesso</string>\n    <string name=\"manage_no_modules\">Ainda sem módulos</string>\n    <string name=\"manage_module_settings\">Configurações do módulo</string>\n    <string name=\"manage_app_info\">Informação da aplicação</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Novo Patch</string>\n    <string name=\"patch_select_dir_title\">Selecione o diretório de armazenamento</string>\n    <string name=\"patch_select_dir_text\">Selecione um diretório para armazenar os apks corrigidos</string>\n    <string name=\"patch_select_dir_error\">Erro ao definir o diretório de armazenamento</string>\n    <string name=\"patch_from_storage\">Selecione apk(s) do armazenamento</string>\n    <string name=\"patch_from_applist\">Selecione um aplicativo instalado</string>\n    <string name=\"patch_mode\">Modo Patch</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Corrigir uma aplicação sem módulos incorporados.\\nO escopo do Xposed pode ser alterado dinamicamente sem a necessidade de re-patch.\\nOs aplicativos locais corrigidos só podem ser executados no dispositivo local.</string>\n    <string name=\"patch_integrated\">Integrado</string>\n    <string name=\"patch_integrated_desc\">Corrija um aplicativo com módulos incorporados.\\nO aplicativo corrigido pode ser executado sem o gerenciador, mas não pode ser gerenciado dinamicamente.\\nAplicativos corrigidos integrados podem ser usados em dispositivos que não possuem o LSPatch Manager instalado.</string>\n    <string name=\"patch_embed_modules\">Incorporar módulos</string>\n    <string name=\"patch_debuggable\">Depurável</string>\n    <string name=\"patch_sigbypass\">Bypass de assinatura</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Desligado</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Ignorar PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Ignorar PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Substituir o código da versão</string>\n    <string name=\"patch_override_version_code_desc\">Substitua o código de versão do aplicativo corrigido para 1\\nIsso permite a instalação de downgrade no futuro, e geralmente isso não afetará o código de versão realmente percebido pela aplicação</string>\n    <string name=\"patch_start\">Iniciar patch</string>\n    <string name=\"patch_return\">Retornar</string>\n    <string name=\"patch_uninstall_text\">Devido a assinaturas diferentes, você precisa desinstalar o aplicativo original antes de instalar o corrigido.\\nCertifique-se de ter feito backup dos dados pessoais.</string>\n    <string name=\"patch_install_successfully\">Instalar com sucesso</string>\n    <string name=\"patch_install_failed\">Falha na instalação</string>\n    <string name=\"patch_no_xposed_module\">Nenhum módulo Xposed foi encontrado</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Seleccionar aplicações</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Definições</string>\n    <string name=\"settings_keystore\">Chaveiro de Assinatura</string>\n    <string name=\"settings_keystore_default\">Incorporado em</string>\n    <string name=\"settings_keystore_custom\">Personalizado</string>\n    <string name=\"settings_keystore_dialog_title\">Chaveiro personalizado</string>\n    <string name=\"settings_keystore_file\">Arquivo da Keystore</string>\n    <string name=\"settings_keystore_password\">Senha</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Palavra-passe</string>\n    <string name=\"settings_keystore_desc\">Set keystore (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Tipo errado de chaveiro</string>\n    <string name=\"settings_keystore_wrong_password\">Palavra-chave de loja errada</string>\n    <string name=\"settings_keystore_wrong_alias\">Nome falso</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Palavra-passe de outrora incorrecta</string>\n    <string name=\"settings_detail_patch_logs\">Registos de remendos detalhados</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-pt-rBR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Adicionar</string>\n    <string name=\"install\">Instalar</string>\n    <string name=\"installing\">Instalando</string>\n    <string name=\"uninstall\">Desinstalar</string>\n    <string name=\"uninstalling\">Desinstalando</string>\n    <string name=\"copy_error\">Erro de cópia</string>\n    <string name=\"apps\">Apps</string>\n    <string name=\"modules\">Módulos</string>\n    <string name=\"shizuku_available\">Serviço Shizuku disponível</string>\n    <string name=\"shizuku_unavailable\">Serviço Shizuku não conectado</string>\n    <string name=\"screen_repo\">Repositório</string>\n    <string name=\"screen_logs\">Registos</string>\n    <string name=\"off\">Desligado</string>\n    <string name=\"error_unknown\">Erro desconhecido</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Algumas funções indisponíveis</string>\n    <string name=\"home_api_version\">Versão da API</string>\n    <string name=\"home_lspatch_version\">Versão do LSPatch</string>\n    <string name=\"home_framework_version\">Versão do Framework</string>\n    <string name=\"home_system_version\">Versão do sistema</string>\n    <string name=\"home_device\">Dispositivo</string>\n    <string name=\"home_system_abi\">Sistema ABI</string>\n    <string name=\"home_info_copied\">Copiado para a área de transferência</string>\n    <string name=\"home_support\">Apoiar</string>\n    <string name=\"home_description\">LSPatch é uma estrutura Xposed gratuita e não root baseada no núcleo LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Veja o código-fonte em %1$s<br/>Junte-se ao nosso canal %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gerenciar</string>\n    <string name=\"manage_loading\">Carregando</string>\n    <string name=\"manage_no_apps\">Nenhum app corrigido ainda</string>\n    <string name=\"manage_rolling\">Rolando</string>\n    <string name=\"manage_update_loader\">Atualizar carregador</string>\n    <string name=\"manage_update_loader_successfully\">Atualizado com sucesso</string>\n    <string name=\"manage_update_loader_failed\">Atualização falhou</string>\n    <string name=\"manage_module_scope\">Escopo do módulo</string>\n    <string name=\"manage_optimize\">Otimizar</string>\n    <string name=\"manage_optimize_successfully\">Otimizado com sucesso</string>\n    <string name=\"manage_optimize_failed\">Otimização falhou</string>\n    <string name=\"manage_uninstall_successfully\">Desinstalado com sucesso</string>\n    <string name=\"manage_no_modules\">Nenhum módulo ainda</string>\n    <string name=\"manage_module_settings\">Configurações do módulo</string>\n    <string name=\"manage_app_info\">Informações do app</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Novo Patch</string>\n    <string name=\"patch_select_dir_title\">Selecione o diretório de armazenamento</string>\n    <string name=\"patch_select_dir_text\">Selecione um diretório para armazenar os apks corrigidos</string>\n    <string name=\"patch_select_dir_error\">Erro ao configurar o diretório de armazenamento</string>\n    <string name=\"patch_from_storage\">Selecione apk(s) do armazenamento</string>\n    <string name=\"patch_from_applist\">Selecione um app instalado</string>\n    <string name=\"patch_mode\">Modo Patch</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Faça o patch de um aplicativo sem módulos incorporados.\\nO escopo do Xposed pode ser alterado dinamicamente sem nova correção.\\nAplicativos com patches locais só podem ser executados no dispositivo local.</string>\n    <string name=\"patch_integrated\">Integrado</string>\n    <string name=\"patch_integrated_desc\">Corrija um app com módulos incorporados.\\nO app corrigido pode ser executado sem o gerenciador, mas não pode ser gerenciado dinamicamente.\\nApps corrigidos integrados podem ser usados ​​em dispositivos que não têm o LSPatch Manager instalado.</string>\n    <string name=\"patch_embed_modules\">Incorporar módulos</string>\n    <string name=\"patch_debuggable\">Depurável</string>\n    <string name=\"patch_sigbypass\">Ignorar assinatura</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Desligado</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Ignorar PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Ignorar PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Substituir código da versão</string>\n    <string name=\"patch_override_version_code_desc\">Substitua o código da versão do app corrigido para 1.\\nIsso permite a instalação de downgrade no futuro e, geralmente, isso não afetará o código da versão realmente percebido pelo app.</string>\n    <string name=\"patch_start\">Iniciar patch</string>\n    <string name=\"patch_return\">Retornar</string>\n    <string name=\"patch_uninstall_text\">Devido às diferentes assinaturas, você precisa desinstalar o app original antes de instalar o corrigido.\\nCertifique-se de ter feito backup dos dados pessoais.</string>\n    <string name=\"patch_install_successfully\">Instalado com sucesso</string>\n    <string name=\"patch_install_failed\">Falha na instalação</string>\n    <string name=\"patch_no_xposed_module\">Nenhum módulo Xposed foi encontrado</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Selecionar apps</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Configurações</string>\n    <string name=\"settings_keystore\">Armazenamento de chaves de assinatura</string>\n    <string name=\"settings_keystore_default\">Construídas em</string>\n    <string name=\"settings_keystore_custom\">Personalizado</string>\n    <string name=\"settings_keystore_dialog_title\">Armazenamento de chaves personalizado</string>\n    <string name=\"settings_keystore_file\">Arquivo de armazenamento de chaves</string>\n    <string name=\"settings_keystore_password\">Senha</string>\n    <string name=\"settings_keystore_alias\">Apelido</string>\n    <string name=\"settings_keystore_alias_password\">Senha do apelido</string>\n    <string name=\"settings_keystore_desc\">Definir armazenamento de chaves (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Tipo errado de armazenamento de chaves</string>\n    <string name=\"settings_keystore_wrong_password\">Senha de armazenamento de chaves incorreta</string>\n    <string name=\"settings_keystore_wrong_alias\">Nome do apelido incorreto</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Senha do apelido incorreta</string>\n    <string name=\"settings_detail_patch_logs\">Registros de patch detalhados</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ro/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Adăuga</string>\n    <string name=\"install\">Instalați</string>\n    <string name=\"installing\">Instalarea</string>\n    <string name=\"uninstall\">Dezinstalați</string>\n    <string name=\"uninstalling\">Dezinstalarea</string>\n    <string name=\"copy_error\">Eroare de copiere</string>\n    <string name=\"apps\">Aplicații</string>\n    <string name=\"modules\">Module</string>\n    <string name=\"shizuku_available\">Serviciu Shizuku disponibil</string>\n    <string name=\"shizuku_unavailable\">Serviciul Shizuku nu este conectat</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Bușteni</string>\n    <string name=\"off\">Off</string>\n    <string name=\"error_unknown\">Eroare necunoscută</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Unele funcții nu sunt disponibile</string>\n    <string name=\"home_api_version\">Versiunea API</string>\n    <string name=\"home_lspatch_version\">Versiunea LSPatch</string>\n    <string name=\"home_framework_version\">Versiunea cadru</string>\n    <string name=\"home_system_version\">Versiunea de sistem</string>\n    <string name=\"home_device\">Dispozitiv</string>\n    <string name=\"home_system_abi\">Sistem ABI</string>\n    <string name=\"home_info_copied\">Copiat în clipboard</string>\n    <string name=\"home_support\">A sustine</string>\n    <string name=\"home_description\">LSPatch este un cadru Xposed gratuit non-root, bazat pe nucleul LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Vizualizați codul sursă la %1$s<br/>Alăturați-vă canalului nostru %2$s]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Gestionați</string>\n    <string name=\"manage_loading\">Se încarcă</string>\n    <string name=\"manage_no_apps\">Nicio aplicație corecţionată încă</string>\n    <string name=\"manage_rolling\">Rostogolire</string>\n    <string name=\"manage_update_loader\">Actualizarea încărcătorului</string>\n    <string name=\"manage_update_loader_successfully\">Actualizare cu succes</string>\n    <string name=\"manage_update_loader_failed\">Actualizarea a eșuat</string>\n    <string name=\"manage_module_scope\">Domeniul de aplicare al modulului</string>\n    <string name=\"manage_optimize\">Optimizați</string>\n    <string name=\"manage_optimize_successfully\">Optimizați cu succes</string>\n    <string name=\"manage_optimize_failed\">Optimizarea a eșuat</string>\n    <string name=\"manage_uninstall_successfully\">Dezinstalare cu succes</string>\n    <string name=\"manage_no_modules\">Nu există încă module</string>\n    <string name=\"manage_module_settings\">Setări ale modulelor</string>\n    <string name=\"manage_app_info\">Informații despre aplicație</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Patch nou</string>\n    <string name=\"patch_select_dir_title\">Selectați directorul de stocare</string>\n    <string name=\"patch_select_dir_text\">Selectați un director pentru a stoca apk-urile corectate</string>\n    <string name=\"patch_select_dir_error\">Eroare la setarea directorului de stocare</string>\n    <string name=\"patch_from_storage\">Selectați apk-uri din stocare</string>\n    <string name=\"patch_from_applist\">Selectați o aplicație instalată</string>\n    <string name=\"patch_mode\">Modul Patch</string>\n    <string name=\"patch_local\">Local</string>\n    <string name=\"patch_local_desc\">Remediați o aplicație fără module încorporate.\\nDomeniul de aplicare Xposed poate fi modificat în mod dinamic fără a fi nevoie de re-patch.\\nAplicațiile patch-uite local pot rula numai pe dispozitivul local.</string>\n    <string name=\"patch_integrated\">Integrat</string>\n    <string name=\"patch_integrated_desc\">Corectați o aplicație cu module încorporate.\\nAplicația corectată poate rula fără manager, dar nu poate fi gestionată dinamic.\\nAplicațiile integrate cu corecții pot fi utilizate pe dispozitivele care nu au LSPatch Manager instalat.</string>\n    <string name=\"patch_embed_modules\">Încorporați module</string>\n    <string name=\"patch_debuggable\">Depanabil</string>\n    <string name=\"patch_sigbypass\">Ocolire semnătură</string>\n    <string name=\"patch_sigbypasslv0\">lv0: oprit</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Ocoli PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bypass PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Ignorați codul versiunii</string>\n    <string name=\"patch_override_version_code_desc\">Suprascrieți codul de versiune al aplicației corectate la 1\\nAcest lucru permite o instalare retrogradată în viitor și, în general, acest lucru nu va afecta codul de versiune perceput efectiv de aplicație.</string>\n    <string name=\"patch_start\">Începeți Patch-ul</string>\n    <string name=\"patch_return\">Întoarcere</string>\n    <string name=\"patch_uninstall_text\">Din cauza semnăturilor diferite, trebuie să dezinstalați aplicația originală înainte de a o instala pe cea corectată.\\nAsigurați-vă că ați făcut o copie de rezervă a datelor personale.</string>\n    <string name=\"patch_install_successfully\">Instalați cu succes</string>\n    <string name=\"patch_install_failed\">Instalarea a eșuat</string>\n    <string name=\"patch_no_xposed_module\">Nu au fost găsite module Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Selectați aplicații</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Setări</string>\n    <string name=\"settings_keystore\">Keystore de semnături</string>\n    <string name=\"settings_keystore_default\">Built-in</string>\n    <string name=\"settings_keystore_custom\">Personalizat</string>\n    <string name=\"settings_keystore_dialog_title\">Keystore personalizat</string>\n    <string name=\"settings_keystore_file\">Fișier Keystore</string>\n    <string name=\"settings_keystore_password\">Parola</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias parola</string>\n    <string name=\"settings_keystore_desc\">Setați keystore (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Tip greșit de depozit de chei</string>\n    <string name=\"settings_keystore_wrong_password\">Parolă greșită pentru keystore</string>\n    <string name=\"settings_keystore_wrong_alias\">Nume alias greșit</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Parolă alias greșită</string>\n    <string name=\"settings_detail_patch_logs\">Detaliile jurnalelor de patch-uri</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Добавить</string>\n    <string name=\"install\">Установите</string>\n    <string name=\"installing\">Установка</string>\n    <string name=\"uninstall\">Удалить</string>\n    <string name=\"uninstalling\">Удаление</string>\n    <string name=\"copy_error\">Ошибка копирования</string>\n    <string name=\"apps\">Приложения</string>\n    <string name=\"modules\">Модули</string>\n    <string name=\"shizuku_available\">Служба Shizuku доступна</string>\n    <string name=\"shizuku_unavailable\">Служба Shizuku не подключена</string>\n    <string name=\"screen_repo\">Репозиторий</string>\n    <string name=\"screen_logs\">Логи</string>\n    <string name=\"off\">Выкл.</string>\n    <string name=\"error_unknown\">Неизвестная ошибка</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Некоторые функции недоступны</string>\n    <string name=\"home_api_version\">Версия API</string>\n    <string name=\"home_lspatch_version\">Версия LSPatch</string>\n    <string name=\"home_framework_version\">Версия фреймворк</string>\n    <string name=\"home_system_version\">Версия Android</string>\n    <string name=\"home_device\">Устройство</string>\n    <string name=\"home_system_abi\">Разрядность системы (ABI)</string>\n    <string name=\"home_info_copied\">Скопировано в буфер обмена</string>\n    <string name=\"home_support\">О приложении</string>\n    <string name=\"home_description\">LSPatch — это бесплатный фреймворк Xposed без полномочий root, основанный на ядре LSPosed</string>\n    <string name=\"home_view_source_code\"><![CDATA[Посмотреть исходный код на %1$s<br/>Присоединяйтесь к нашему %2$s каналу]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Управлять</string>\n    <string name=\"manage_loading\">Загрузка</string>\n    <string name=\"manage_no_apps\">Пропатченных приложений пока нет</string>\n    <string name=\"manage_rolling\">Прокат</string>\n    <string name=\"manage_update_loader\">Обновление загрузчика</string>\n    <string name=\"manage_update_loader_successfully\">Успешное обновление</string>\n    <string name=\"manage_update_loader_failed\">Обновление не удалось</string>\n    <string name=\"manage_module_scope\">Область применения модуля</string>\n    <string name=\"manage_optimize\">Оптимизировать</string>\n    <string name=\"manage_optimize_successfully\">Оптимизация завершена!</string>\n    <string name=\"manage_optimize_failed\">Ошибка оптимизации</string>\n    <string name=\"manage_uninstall_successfully\">Удаление завершено!</string>\n    <string name=\"manage_no_modules\">Модулей пока нет</string>\n    <string name=\"manage_module_settings\">Настройки модуля</string>\n    <string name=\"manage_app_info\">Информация о приложении</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Новый патч</string>\n    <string name=\"patch_select_dir_title\">Выберите каталог хранения</string>\n    <string name=\"patch_select_dir_text\">Выберите каталог для хранения пропатченных Apk</string>\n    <string name=\"patch_select_dir_error\">Ошибка при настройке каталога хранения</string>\n    <string name=\"patch_from_storage\">Выберите apk из хранилища</string>\n    <string name=\"patch_from_applist\">Выберите установленное приложение</string>\n    <string name=\"patch_mode\">Режим патча</string>\n    <string name=\"patch_local\">Локальный</string>\n    <string name=\"patch_local_desc\">Патч приложения без встроенных модулей.\\nОбласть действия Xposed может быть изменена динамически без повторного исправления.\\nЛокально пропатченные приложения могут работать только на локальном устройстве.</string>\n    <string name=\"patch_integrated\">Интегрированный</string>\n    <string name=\"patch_integrated_desc\">Патч приложения со встроенными модулями.\\nПатченное приложение может работать без менеджера, но не может управляться динамически.\\nПортативные патченные приложения могут быть использованы на устройствах, у которых не установлен LSPatch менеджер</string>\n    <string name=\"patch_embed_modules\">Встроить модули</string>\n    <string name=\"patch_debuggable\">Отладка</string>\n    <string name=\"patch_sigbypass\">Обход подписи</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Выкл.</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Обход PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Обход PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Переопределить код версии</string>\n    <string name=\"patch_override_version_code_desc\">Переопределить код версии исправленного приложения на 1\\nЭто позволит понижать версию установки, и это не повлияет на код версии, фактически воспринимаемый приложением</string>\n    <string name=\"patch_start\">Запустить патч</string>\n    <string name=\"patch_return\">Вернуться</string>\n    <string name=\"patch_uninstall_text\">Из-за разных сигнатур вам необходимо удалить оригинальное приложение перед установкой исправленного.\\nУбедитесь, что у вас есть резервная копия личных данных.</string>\n    <string name=\"patch_install_successfully\">Установка завершена</string>\n    <string name=\"patch_install_failed\">Установка не удалась</string>\n    <string name=\"patch_no_xposed_module\">Не найдено ни одного модуля Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Выберите приложения</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Настройки</string>\n    <string name=\"settings_keystore\">Хранилище ключей подписи</string>\n    <string name=\"settings_keystore_default\">Встроенное</string>\n    <string name=\"settings_keystore_custom\">Пользовательское</string>\n    <string name=\"settings_keystore_dialog_title\">Пользовательское хранилище ключей</string>\n    <string name=\"settings_keystore_file\">Файл хранилища ключей</string>\n    <string name=\"settings_keystore_password\">Пароль</string>\n    <string name=\"settings_keystore_alias\">Псевдоним</string>\n    <string name=\"settings_keystore_alias_password\">Псевдоним пароля</string>\n    <string name=\"settings_keystore_desc\">Установите хранилище ключей (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Неправильный тип хранилища ключей</string>\n    <string name=\"settings_keystore_wrong_password\">Неправильный пароль хранилища ключей</string>\n    <string name=\"settings_keystore_wrong_alias\">Неправильное имя псевдонима</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Неправильный пароль псевдонима</string>\n    <string name=\"settings_detail_patch_logs\">Подробные логи патчей</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-si/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">එකතු කරන්න</string>\n    <string name=\"install\">ස්ථාපනය කරන්න</string>\n    <string name=\"installing\">ස්ථාපනය කිරීම</string>\n    <string name=\"uninstall\">අස්ථාපනය කරන්න</string>\n    <string name=\"uninstalling\">අස්ථාපනය කිරීම</string>\n    <string name=\"copy_error\">පිටපත් කිරීමේ දෝෂයකි</string>\n    <string name=\"apps\">යෙදුම්</string>\n    <string name=\"modules\">මොඩියුල</string>\n    <string name=\"shizuku_available\">Shizku සේවාව ඇත</string>\n    <string name=\"shizuku_unavailable\">Shizku සේවාව සම්බන්ධ නොවේ</string>\n    <string name=\"screen_repo\">රෙපෝ</string>\n    <string name=\"screen_logs\">සටහන්</string>\n    <string name=\"off\">අක්රියයි</string>\n    <string name=\"error_unknown\">නොදන්නා දෝෂයකි</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">සමහර කාර්යයන් නොමැත</string>\n    <string name=\"home_api_version\">API අනුවාදය</string>\n    <string name=\"home_lspatch_version\">LSPatch අනුවාදය</string>\n    <string name=\"home_framework_version\">රාමු අනුවාදය</string>\n    <string name=\"home_system_version\">පද්ධති අනුවාදය</string>\n    <string name=\"home_device\">උපාංගය</string>\n    <string name=\"home_system_abi\">පද්ධතිය ABI</string>\n    <string name=\"home_info_copied\">පසුරු පුවරුවට පිටපත් කර ඇත</string>\n    <string name=\"home_support\">සහාය</string>\n    <string name=\"home_description\">LSPatch යනු LSPosed core මත පදනම් වූ නොමිලේ Root නොවන Xposed රාමුවකි.</string>\n    <string name=\"home_view_source_code\"><![CDATA[මූල කේතය %1$s<br/>ට බලන්න අපගේ %2$s නාලිකාවට සම්බන්ධ වන්න]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">කළමනාකරණය කරන්න</string>\n    <string name=\"manage_loading\">පැටවීම</string>\n    <string name=\"manage_no_apps\">තවම පැච් කළ යෙදුම් නැත</string>\n    <string name=\"manage_rolling\">පෙරළීම</string>\n    <string name=\"manage_update_loader\">යාවත්කාලීන ලෝඩරය</string>\n    <string name=\"manage_update_loader_successfully\">සාර්ථකව යාවත්කාලීන කරන්න</string>\n    <string name=\"manage_update_loader_failed\">යාවත්කාලීන කිරීම අසාර්ථක විය</string>\n    <string name=\"manage_module_scope\">මොඩියුලය විෂය පථය</string>\n    <string name=\"manage_optimize\">ප්‍රශස්ත කරන්න</string>\n    <string name=\"manage_optimize_successfully\">සාර්ථකව ප්‍රශස්ත කරන්න</string>\n    <string name=\"manage_optimize_failed\">ප්‍රශස්ත කිරීම අසාර්ථක විය</string>\n    <string name=\"manage_uninstall_successfully\">සාර්ථකව අස්ථාපනය කරන්න</string>\n    <string name=\"manage_no_modules\">තවම මොඩියුල නැත</string>\n    <string name=\"manage_module_settings\">මොඩියුල සැකසුම්</string>\n    <string name=\"manage_app_info\">යෙදුම් තොරතුරු</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">නව පැච්</string>\n    <string name=\"patch_select_dir_title\">ගබඩා නාමාවලිය තෝරන්න</string>\n    <string name=\"patch_select_dir_text\">පැච් කරන ලද apks ගබඩා කිරීමට නාමාවලියක් තෝරන්න</string>\n    <string name=\"patch_select_dir_error\">ගබඩා නාමාවලිය සැකසීමේදී දෝෂයකි</string>\n    <string name=\"patch_from_storage\">ගබඩාවෙන් apk(s) තෝරන්න</string>\n    <string name=\"patch_from_applist\">ස්ථාපිත යෙදුමක් තෝරන්න</string>\n    <string name=\"patch_mode\">පැච් මාදිලිය</string>\n    <string name=\"patch_local\">දේශීය</string>\n    <string name=\"patch_local_desc\">කාවැද්දූ මොඩියුල නොමැති යෙදුමක් පැච් කරන්න.\\nXposed විෂය පථය නැවත පැච් කිරීමකින් තොරව ගතිකව වෙනස් කළ හැක.\\nදේශීය පැච් කළ යෙදුම් ධාවනය කළ හැක්කේ දේශීය උපාංගයේ පමණි.</string>\n    <string name=\"patch_integrated\">ඒකාබද්ධ</string>\n    <string name=\"patch_integrated_desc\">කාවැද්දූ මොඩියුල සහිත යෙදුමක් පැච් කරන්න.\\nපැච් කළ යෙදුම කළමනාකරු නොමැතිව ධාවනය කළ හැකි නමුත් ගතිකව කළමනාකරණය කළ නොහැක.\\nLSPatch Manager ස්ථාපනය කර නොමැති උපාංග මත ඒකාබද්ධ පැච් යෙදුම් භාවිතා කළ හැක.</string>\n    <string name=\"patch_embed_modules\">මොඩියුල තැන්පත් කරන්න</string>\n    <string name=\"patch_debuggable\">නිදොස් කළ හැකි</string>\n    <string name=\"patch_sigbypass\">අත්සන බයිපාස්</string>\n    <string name=\"patch_sigbypasslv0\">lv0: අක්‍රියයි</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM බයිපාස් කරන්න</string>\n    <string name=\"patch_sigbypasslv2\">lv2: බයිපාස් PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">අනුවාද කේතය අභිබවා යන්න</string>\n    <string name=\"patch_override_version_code_desc\">පැච් කරන ලද යෙදුමේ අනුවාද කේතය 1\\nවෙත ප්‍රතික්‍ෂේප කරන්න මෙය අනාගතයේදී ස්ථාපනය පහත හෙලීමට ඉඩ සලසයි, සාමාන්‍යයෙන් මෙය යෙදුමට සත්‍ය වශයෙන්ම පෙනෙන අනුවාද කේතයට බලපාන්නේ නැත.</string>\n    <string name=\"patch_start\">පැච් ආරම්භ කරන්න</string>\n    <string name=\"patch_return\">ආපසු</string>\n    <string name=\"patch_uninstall_text\">විවිධ අත්සන් හේතුවෙන්, පැච් කළ එක ස්ථාපනය කිරීමට පෙර ඔබට මුල් යෙදුම අස්ථාපනය කිරීමට අවශ්‍ය වේ.\\nඔබ පුද්ගලික දත්ත උපස්ථ කර ඇති බවට වග බලා ගන්න.</string>\n    <string name=\"patch_install_successfully\">සාර්ථකව ස්ථාපනය කරන්න</string>\n    <string name=\"patch_install_failed\">ස්ථාපනය අසාර්ථක විය</string>\n    <string name=\"patch_no_xposed_module\">Xposed මොඩියුල(ය) හමු නොවිණි</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">යෙදුම් තෝරන්න</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">සැකසුම්</string>\n    <string name=\"settings_keystore\">අත්සන යතුරු ගබඩාව</string>\n    <string name=\"settings_keystore_default\">බිල්ට්-ඉන්</string>\n    <string name=\"settings_keystore_custom\">අභිරුචි</string>\n    <string name=\"settings_keystore_dialog_title\">අභිරුචි යතුරු ගබඩාව</string>\n    <string name=\"settings_keystore_file\">යතුරු ගබඩා ගොනුව</string>\n    <string name=\"settings_keystore_password\">මුරපදය</string>\n    <string name=\"settings_keystore_alias\">අන්වර්ථ නාමය</string>\n    <string name=\"settings_keystore_alias_password\">අන්වර්ථ මුරපදය</string>\n    <string name=\"settings_keystore_desc\">යතුරු ගබඩාව සකසන්න (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">වැරදි ආකාරයේ යතුරු ගබඩාවක්</string>\n    <string name=\"settings_keystore_wrong_password\">වැරදි යතුරු ගබඩා මුරපදය</string>\n    <string name=\"settings_keystore_wrong_alias\">වැරදි අන්වර්ථ නාමයක්</string>\n    <string name=\"settings_keystore_wrong_alias_password\">වැරදි අන්වර්ථ මුරපදය</string>\n    <string name=\"settings_detail_patch_logs\">විස්තර පැච් ලොග</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-sk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Pridať</string>\n    <string name=\"install\">Inštalácia</string>\n    <string name=\"installing\">Inštaluje sa</string>\n    <string name=\"uninstall\">Odinštalovať</string>\n    <string name=\"uninstalling\">Prebieha odinštalovanie</string>\n    <string name=\"copy_error\">Chyba kopírovania</string>\n    <string name=\"apps\">Aplikácie</string>\n    <string name=\"modules\">Moduly</string>\n    <string name=\"shizuku_available\">K dispozícii je služba Shizuku</string>\n    <string name=\"shizuku_unavailable\">Služba Shizuku nie je pripojená</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Denníky</string>\n    <string name=\"off\">Vypnuté</string>\n    <string name=\"error_unknown\">Neznáma chyba</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Niektoré funkcie sú nedostupné</string>\n    <string name=\"home_api_version\">Verzia API</string>\n    <string name=\"home_lspatch_version\">Verzia LSPatch</string>\n    <string name=\"home_framework_version\">Verzia rámca</string>\n    <string name=\"home_system_version\">Verzia systému</string>\n    <string name=\"home_device\">Zariadenie</string>\n    <string name=\"home_system_abi\">Systém ABI</string>\n    <string name=\"home_info_copied\">Skopírované do schránky</string>\n    <string name=\"home_support\">podpora</string>\n    <string name=\"home_description\">LSPatch je bezplatný rámec Xposed bez rootov založený na jadre LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Zobraziť zdrojový kód na %1$s<br/>Pripojte sa k nášmu %2$s kanálu]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Spravovať</string>\n    <string name=\"manage_loading\">Načítava</string>\n    <string name=\"manage_no_apps\">Zatiaľ žiadne opravené aplikácie</string>\n    <string name=\"manage_rolling\">Rolovanie</string>\n    <string name=\"manage_update_loader\">Aktualizujte nakladač</string>\n    <string name=\"manage_update_loader_successfully\">Aktualizácia bola úspešná</string>\n    <string name=\"manage_update_loader_failed\">Aktualizacia neuspešná</string>\n    <string name=\"manage_module_scope\">Rozsah modulu</string>\n    <string name=\"manage_optimize\">Optimalizovať</string>\n    <string name=\"manage_optimize_successfully\">Úspešná optimalizácia</string>\n    <string name=\"manage_optimize_failed\">Optimalizácia zlyhala</string>\n    <string name=\"manage_uninstall_successfully\">Odinštalovanie bolo úspešné</string>\n    <string name=\"manage_no_modules\">Zatiaľ žiadne moduly</string>\n    <string name=\"manage_module_settings\">Nastavenia modulu</string>\n    <string name=\"manage_app_info\">Informácie o aplikácii</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Nový Patch</string>\n    <string name=\"patch_select_dir_title\">Vyberte priečinok úložiska</string>\n    <string name=\"patch_select_dir_text\">Vyberte adresár, do ktorého chcete uložiť opravené súbory APK</string>\n    <string name=\"patch_select_dir_error\">Chyba pri nastavovaní priečinka úložiska</string>\n    <string name=\"patch_from_storage\">Vyberte apk z úložiska</string>\n    <string name=\"patch_from_applist\">Vyberte nainštalovanú aplikáciu</string>\n    <string name=\"patch_mode\">Režim opravy</string>\n    <string name=\"patch_local\">Miestne</string>\n    <string name=\"patch_local_desc\">Oprava aplikácie bez vložených modulov.\\nRozsah Xposed možno dynamicky meniť bez opätovného záplatovania.\\nLokálne opravené aplikácie môžu byť spustené len na lokálnom zariadení.</string>\n    <string name=\"patch_integrated\">Integrovaný</string>\n    <string name=\"patch_integrated_desc\">Opravte aplikáciu so vstavanými modulmi.\\nOpravená aplikácia môže bežať bez správcu, ale nedá sa spravovať dynamicky.\\nIntegrované opravené aplikácie možno použiť na zariadeniach, ktoré nemajú nainštalovaný LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Vložiť moduly</string>\n    <string name=\"patch_debuggable\">Laditeľné</string>\n    <string name=\"patch_sigbypass\">Obídenie podpisu</string>\n    <string name=\"patch_sigbypasslv0\">lv0: vypnuté</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Obíďte PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Obísť PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Prepísať kód verzie</string>\n    <string name=\"patch_override_version_code_desc\">Prepísať kód verzie opravenej aplikácie na 1\\nUmožňuje to v budúcnosti inštaláciu na nižšiu verziu a vo všeobecnosti to neovplyvní kód verzie skutočne vnímaný aplikáciou</string>\n    <string name=\"patch_start\">Spustite opravu</string>\n    <string name=\"patch_return\">Návrat</string>\n    <string name=\"patch_uninstall_text\">Kvôli rôznym podpisom musíte pred inštaláciou opravenej aplikácie odinštalovať pôvodnú aplikáciu.\\nUistite sa, že máte zálohované osobné údaje.</string>\n    <string name=\"patch_install_successfully\">Nainštalujte úspešne</string>\n    <string name=\"patch_install_failed\">Inštalácia zlyhala</string>\n    <string name=\"patch_no_xposed_module\">Neboli nájdené žiadne moduly Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Vyberte položku Aplikácie</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">nastavenie</string>\n    <string name=\"settings_keystore\">Úložisko podpisových kľúčov</string>\n    <string name=\"settings_keystore_default\">Vstavaný</string>\n    <string name=\"settings_keystore_custom\">Vlastné</string>\n    <string name=\"settings_keystore_dialog_title\">Vlastné úložisko kľúčov</string>\n    <string name=\"settings_keystore_file\">Súbor úložiska kľúčov</string>\n    <string name=\"settings_keystore_password\">heslo</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Heslo aliasu</string>\n    <string name=\"settings_keystore_desc\">Nastaviť úložisko kľúčov (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Nesprávny typ úložiska kľúčov</string>\n    <string name=\"settings_keystore_wrong_password\">Nesprávne heslo úložiska kľúčov</string>\n    <string name=\"settings_keystore_wrong_alias\">Nesprávny alias</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Nesprávne heslo aliasu</string>\n    <string name=\"settings_detail_patch_logs\">Podrobné protokoly o opravách</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-sv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Lägg till</string>\n    <string name=\"install\">Installera</string>\n    <string name=\"installing\">Installerar</string>\n    <string name=\"uninstall\">Avinstallera</string>\n    <string name=\"uninstalling\">Avinstallera</string>\n    <string name=\"copy_error\">Kopieringsfel</string>\n    <string name=\"apps\">Appar</string>\n    <string name=\"modules\">Moduler</string>\n    <string name=\"shizuku_available\">Shizuku-tjänsten är tillgänglig</string>\n    <string name=\"shizuku_unavailable\">Shizuku-tjänsten är inte ansluten</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Loggar</string>\n    <string name=\"off\">Av</string>\n    <string name=\"error_unknown\">Okänt fel</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Vissa funktioner är inte tillgängliga</string>\n    <string name=\"home_api_version\">API-version</string>\n    <string name=\"home_lspatch_version\">LSPatch-version</string>\n    <string name=\"home_framework_version\">Ramverksversion</string>\n    <string name=\"home_system_version\">Systemversion</string>\n    <string name=\"home_device\">Enhet</string>\n    <string name=\"home_system_abi\">System ABI</string>\n    <string name=\"home_info_copied\">Kopierat till urklipp</string>\n    <string name=\"home_support\">Stöd</string>\n    <string name=\"home_description\">LSPatch är ett gratis, icke-root Xposed-ramverk baserad på LSPosed-kärnan.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Visa källkoden på %1$s<br/>Gå med i vår %2$s -kanal]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Hantera</string>\n    <string name=\"manage_loading\">Laddar</string>\n    <string name=\"manage_no_apps\">Inga patchade appar ännu</string>\n    <string name=\"manage_rolling\">Rullande</string>\n    <string name=\"manage_update_loader\">Uppdatera loader</string>\n    <string name=\"manage_update_loader_successfully\">Uppdatering lyckades</string>\n    <string name=\"manage_update_loader_failed\">Uppdateringen misslyckades</string>\n    <string name=\"manage_module_scope\">Modulens omfattning</string>\n    <string name=\"manage_optimize\">Optimera</string>\n    <string name=\"manage_optimize_successfully\">Optimeringen lyckades</string>\n    <string name=\"manage_optimize_failed\">Optimeringen misslyckades</string>\n    <string name=\"manage_uninstall_successfully\">Avinstalleringen lyckades</string>\n    <string name=\"manage_no_modules\">Inga moduler ännu</string>\n    <string name=\"manage_module_settings\">Modulinställningar</string>\n    <string name=\"manage_app_info\">Om appen</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Ny patch</string>\n    <string name=\"patch_select_dir_title\">Välj lagringsmapp</string>\n    <string name=\"patch_select_dir_text\">Välj en mapp för att lagra de patchade Apk-filerna</string>\n    <string name=\"patch_select_dir_error\">Ett fel uppstod vid inställningen av lagringsmapp</string>\n    <string name=\"patch_from_storage\">Välj Apk-fil(er) från lagringen</string>\n    <string name=\"patch_from_applist\">Välj en installerad app</string>\n    <string name=\"patch_mode\">Patch-läge</string>\n    <string name=\"patch_local\">Lokal</string>\n    <string name=\"patch_local_desc\">Patcha en app utan inbäddade moduler.\\nXposed scope kan ändras dynamiskt utan ny patchning.\\nLokalt patchade appar kan endast köras på den lokala enheten.</string>\n    <string name=\"patch_integrated\">Integrerad</string>\n    <string name=\"patch_integrated_desc\">Patcha en app med inbäddade moduler.\\nDen patchade appen kan köras utan behovet av hantering, men kan då inte hanteras dynamiskt.\\nIntegrerade patchade appar kan användas på enheter som inte har LSPatch Manager installerat.</string>\n    <string name=\"patch_embed_modules\">Inbäddade moduler</string>\n    <string name=\"patch_debuggable\">Felsökningsbar</string>\n    <string name=\"patch_sigbypass\">Hoppa över signaturkontrollen</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Av</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Åtsidosätt pakethanteraren</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Åtsidosätt pakethanteraren + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Åsidosätt versionskoden</string>\n    <string name=\"patch_override_version_code_desc\">Åsidosätt den patchade appens versionskod till 1.\\nDetta tillåter nedgraderad installation i framtiden och generellt påverkar detta inte den versionskod som faktiskt uppfattas av applikationen</string>\n    <string name=\"patch_start\">Påbörja patchning</string>\n    <string name=\"patch_return\">Återgå</string>\n    <string name=\"patch_uninstall_text\">På grund av olika signaturer måste du avinstallera den ursprungliga appen innan du installerar den korrigerade.\\nSe till att du har säkerhetskopierat personlig data.</string>\n    <string name=\"patch_install_successfully\">Installationen lyckades</string>\n    <string name=\"patch_install_failed\">Installationen misslyckades</string>\n    <string name=\"patch_no_xposed_module\">Inga Xposed-modul(er) hittades</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Välj appar</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Inställningar</string>\n    <string name=\"settings_keystore\">Arkiv för signaturnycklar</string>\n    <string name=\"settings_keystore_default\">Inbyggt</string>\n    <string name=\"settings_keystore_custom\">Anpassad</string>\n    <string name=\"settings_keystore_dialog_title\">Anpassat nyckelarkiv</string>\n    <string name=\"settings_keystore_file\">Nyckelarkivfil</string>\n    <string name=\"settings_keystore_password\">Lösenord</string>\n    <string name=\"settings_keystore_alias\">Alias</string>\n    <string name=\"settings_keystore_alias_password\">Alias-lösenord</string>\n    <string name=\"settings_keystore_desc\">Ställ in nyckelarkiv (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Fel typ av nyckelarkiv</string>\n    <string name=\"settings_keystore_wrong_password\">Fel lösenord till nyckelarkivet</string>\n    <string name=\"settings_keystore_wrong_alias\">Fel aliasnamn</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Fel lösenord för alias</string>\n    <string name=\"settings_detail_patch_logs\">Detaljerade patchloggar</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-th/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Addไทย</string>\n    <string name=\"install\">ติดตั้ง</string>\n    <string name=\"installing\">กำลังติดตั้ง</string>\n    <string name=\"uninstall\">ถอนการติดตั้ง</string>\n    <string name=\"uninstalling\">กำลังถอนการติดตั้ง</string>\n    <string name=\"copy_error\">ข้อผิดพลาดในการคัดลอก</string>\n    <string name=\"apps\">แอพ</string>\n    <string name=\"modules\">โมดูล</string>\n    <string name=\"shizuku_available\">ชิซึกุ มีบริการ</string>\n    <string name=\"shizuku_unavailable\">ไม่ได้เชื่อมต่อบริการ Shizuku</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">บันทึก</string>\n    <string name=\"off\">ปิด</string>\n    <string name=\"error_unknown\">Unknown error</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">ฟังก์ชั่นบางอย่างใช้งานไม่ได้</string>\n    <string name=\"home_api_version\">เวอร์ชัน API</string>\n    <string name=\"home_lspatch_version\">เวอร์ชัน LSPatch</string>\n    <string name=\"home_framework_version\">เวอร์ชันกรอบงาน</string>\n    <string name=\"home_system_version\">เวอร์ชันของระบบ</string>\n    <string name=\"home_device\">อุปกรณ์</string>\n    <string name=\"home_system_abi\">ระบบ ABI</string>\n    <string name=\"home_info_copied\">คัดลอกไปยังคลิปบอร์ดแล้ว</string>\n    <string name=\"home_support\">สนับสนุน</string>\n    <string name=\"home_description\">LSPatch เป็นเฟรมเวิร์ก Xposed ที่ไม่ใช่รูทฟรีโดยอิงตามคอร์ LSPosed</string>\n    <string name=\"home_view_source_code\"><![CDATA[ดูซอร์สโค้ดได้ที่ %1$s<br/>เข้าร่วม %2$s ช่องของเรา]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">จัดการ</string>\n    <string name=\"manage_loading\">กำลังโหลด</string>\n    <string name=\"manage_no_apps\">ยังไม่มีแอพแพตช์</string>\n    <string name=\"manage_rolling\">กลิ้ง</string>\n    <string name=\"manage_update_loader\">อัพเดทตัวโหลด</string>\n    <string name=\"manage_update_loader_successfully\">อัพเดทเรียบร้อย</string>\n    <string name=\"manage_update_loader_failed\">การอัพเดทล้มเหลว</string>\n    <string name=\"manage_module_scope\">ขอบเขตโมดูล</string>\n    <string name=\"manage_optimize\">เพิ่มประสิทธิภาพ</string>\n    <string name=\"manage_optimize_successfully\">เพิ่มประสิทธิภาพสำเร็จ</string>\n    <string name=\"manage_optimize_failed\">การเพิ่มประสิทธิภาพล้มเหลว</string>\n    <string name=\"manage_uninstall_successfully\">ถอนการติดตั้งสำเร็จ</string>\n    <string name=\"manage_no_modules\">ยังไม่มีโมดูล</string>\n    <string name=\"manage_module_settings\">การตั้งค่าโมดูล</string>\n    <string name=\"manage_app_info\">ข้อมูลแอพ</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">แพทช์ใหม่</string>\n    <string name=\"patch_select_dir_title\">เลือกไดเร็กทอรีการจัดเก็บ</string>\n    <string name=\"patch_select_dir_text\">เลือกไดเร็กทอรีเพื่อจัดเก็บ apks ที่แพตช์แล้ว</string>\n    <string name=\"patch_select_dir_error\">เกิดข้อผิดพลาดเมื่อตั้งค่าไดเร็กทอรีการจัดเก็บ</string>\n    <string name=\"patch_from_storage\">เลือก apk จากที่เก็บข้อมูล</string>\n    <string name=\"patch_from_applist\">เลือกแอพที่ติดตั้ง</string>\n    <string name=\"patch_mode\">โหมดแพทช์</string>\n    <string name=\"patch_local\">ท้องถิ่น</string>\n    <string name=\"patch_local_desc\">แพทช์แอปที่ไม่มีโมดูลฝังอยู่\\nขอบเขต Xposed สามารถเปลี่ยนแปลงได้แบบไดนามิกโดยไม่ต้องแพตช์ใหม่\\nแอพที่ได้รับการติดตั้งในเครื่องสามารถทำงานได้บนอุปกรณ์ในเครื่องเท่านั้น</string>\n    <string name=\"patch_integrated\">แบบบูรณาการ</string>\n    <string name=\"patch_integrated_desc\">Patch an app with modules embedded.\\nThe patched app can run without the manager, but cannot be managed dynamically.\\nIntegrated patched apps can be used on devices that do not have LSPatch Manager installed.</string>\n    <string name=\"patch_embed_modules\">โมดูลฝังตัว</string>\n    <string name=\"patch_debuggable\">แก้จุดบกพร่องได้</string>\n    <string name=\"patch_sigbypass\">บายพาสลายเซ็น</string>\n    <string name=\"patch_sigbypasslv0\">lv0: ปิด</string>\n    <string name=\"patch_sigbypasslv1\">lv1: บายพาส PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: บายพาส PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">แทนที่รหัสเวอร์ชัน</string>\n    <string name=\"patch_override_version_code_desc\">แทนที่รหัสเวอร์ชันของแอปที่แพตช์เป็น\\nซึ่งช่วยให้สามารถติดตั้งดาวน์เกรดได้ในอนาคต และโดยทั่วไปแล้วสิ่งนี้จะไม่ส่งผลต่อโค้ดเวอร์ชันที่แอปพลิเคชันรับรู้จริงๆ</string>\n    <string name=\"patch_start\">เริ่มโปรแกรมแก้ไข</string>\n    <string name=\"patch_return\">กลับ</string>\n    <string name=\"patch_uninstall_text\">เนื่องจากลายเซ็นต่างกัน คุณต้องถอนการติดตั้งแอปเดิมก่อนที่จะติดตั้งแอปที่แพตช์\\nตรวจสอบให้แน่ใจว่าคุณได้สำรองข้อมูลส่วนบุคคลแล้ว</string>\n    <string name=\"patch_install_successfully\">ติดตั้งสำเร็จ</string>\n    <string name=\"patch_install_failed\">ติดตั้งไม่สำเร็จ</string>\n    <string name=\"patch_no_xposed_module\"></string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">เลือกแอพ</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">การตั้งค่า</string>\n    <string name=\"settings_keystore\">ที่เก็บคีย์ลายเซ็น</string>\n    <string name=\"settings_keystore_default\">ในตัว</string>\n    <string name=\"settings_keystore_custom\">กำหนดเอง</string>\n    <string name=\"settings_keystore_dialog_title\">ที่เก็บคีย์แบบกำหนดเอง</string>\n    <string name=\"settings_keystore_file\">ไฟล์คีย์สโตร์</string>\n    <string name=\"settings_keystore_password\">รหัสผ่าน</string>\n    <string name=\"settings_keystore_alias\">นามแฝง</string>\n    <string name=\"settings_keystore_alias_password\">รหัสผ่านนามแฝง</string>\n    <string name=\"settings_keystore_desc\">ตั้งค่าที่เก็บคีย์ (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">ประเภทของที่เก็บคีย์ผิด</string>\n    <string name=\"settings_keystore_wrong_password\">รหัสผ่านที่เก็บคีย์ไม่ถูกต้อง</string>\n    <string name=\"settings_keystore_wrong_alias\">ชื่อนามแฝงไม่ถูกต้อง</string>\n    <string name=\"settings_keystore_wrong_alias_password\">รหัสผ่านนามแฝงไม่ถูกต้อง</string>\n    <string name=\"settings_detail_patch_logs\">บันทึกการแก้ไขรายละเอียด</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Ekle</string>\n    <string name=\"install\">Düzenlemek</string>\n    <string name=\"installing\">yükleme</string>\n    <string name=\"uninstall\">Kaldır</string>\n    <string name=\"uninstalling\">Kaldırma</string>\n    <string name=\"copy_error\">Kopyalama hatası</string>\n    <string name=\"apps\">Uygulamalar</string>\n    <string name=\"modules\">Modüller</string>\n    <string name=\"shizuku_available\">Shizuku servisi mevcut</string>\n    <string name=\"shizuku_unavailable\">Shizuku hizmeti bağlı değil</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Günlükler</string>\n    <string name=\"off\">Kapalı</string>\n    <string name=\"error_unknown\">Bilinmeyen hata</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Bazı işlevler kullanılamıyor</string>\n    <string name=\"home_api_version\">API Sürümü</string>\n    <string name=\"home_lspatch_version\">LSPatch Sürümü</string>\n    <string name=\"home_framework_version\">Çerçeve Sürümü</string>\n    <string name=\"home_system_version\">Sistem Sürümü</string>\n    <string name=\"home_device\">Cihaz</string>\n    <string name=\"home_system_abi\">Sistem ABI\\'si</string>\n    <string name=\"home_info_copied\">Panoya kopyalandı</string>\n    <string name=\"home_support\">Destek</string>\n    <string name=\"home_description\">LSPatch, LSPosed çekirdeğine dayalı ücretsiz bir kök olmayan Xposed çerçevesidir.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Kaynak kodunu %1$sgörüntüleyin<br/> %2$s kanalımıza katılın]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Yönetmek</string>\n    <string name=\"manage_loading\">Yükleniyor</string>\n    <string name=\"manage_no_apps\">Henüz yama uygulanmış uygulama yok</string>\n    <string name=\"manage_rolling\">Yuvarlanma</string>\n    <string name=\"manage_update_loader\">Yükleyiciyi güncelle</string>\n    <string name=\"manage_update_loader_successfully\">Başarıyla güncelleyin</string>\n    <string name=\"manage_update_loader_failed\">Güncelleme başarısız</string>\n    <string name=\"manage_module_scope\">Modül kapsamı</string>\n    <string name=\"manage_optimize\">optimize et</string>\n    <string name=\"manage_optimize_successfully\">Başarıyla optimize et</string>\n    <string name=\"manage_optimize_failed\">Optimize başarısız oldu</string>\n    <string name=\"manage_uninstall_successfully\">başarıyla kaldır</string>\n    <string name=\"manage_no_modules\">Henüz modül yok</string>\n    <string name=\"manage_module_settings\">Modül ayarları</string>\n    <string name=\"manage_app_info\">Uygulama bilgisi</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Yeni Yama</string>\n    <string name=\"patch_select_dir_title\">Depolama dizini seçin</string>\n    <string name=\"patch_select_dir_text\">Yamalı apk\\'leri saklamak için bir dizin seçin</string>\n    <string name=\"patch_select_dir_error\">Depolama dizini ayarlanırken hata</string>\n    <string name=\"patch_from_storage\">Depodan apk(ler) seçin</string>\n    <string name=\"patch_from_applist\">Yüklü bir uygulama seçin</string>\n    <string name=\"patch_mode\">Yama Modu</string>\n    <string name=\"patch_local\">Yerel</string>\n    <string name=\"patch_local_desc\">Gömülü modüller olmadan bir uygulamayı yamalayın.\\nXposed kapsamı yeniden yama yapılmadan dinamik olarak değiştirilebilir.\\nYerel yamalı uygulamalar yalnızca yerel cihazda çalışabilir.</string>\n    <string name=\"patch_integrated\">Birleşik</string>\n    <string name=\"patch_integrated_desc\">Gömülü modüller içeren bir uygulamaya yama yapın.\\nYamalı uygulama, yönetici olmadan çalışabilir ancak dinamik olarak yönetilemez.\\nTümleşik yamalı uygulamalar, LSPatch Yöneticisi yüklü olmayan cihazlarda kullanılabilir.</string>\n    <string name=\"patch_embed_modules\">Gömülü modüller</string>\n    <string name=\"patch_debuggable\">Hata ayıklanabilir</string>\n    <string name=\"patch_sigbypass\">İmza atlama</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Kapalı</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM\\'yi atla</string>\n    <string name=\"patch_sigbypasslv2\">lv2: PM + openat\\'ı atla (libc)</string>\n    <string name=\"patch_override_version_code\">Sürüm kodunu geçersiz kıl</string>\n    <string name=\"patch_override_version_code_desc\">Yama uygulanmış uygulamanın sürüm kodunu 1\\nolarak geçersiz kıl Bu, gelecekte sürüm düşürme kurulumuna izin verir ve genellikle bu, uygulama tarafından gerçekte algılanan sürüm kodunu etkilemez</string>\n    <string name=\"patch_start\">Yama Başlat</string>\n    <string name=\"patch_return\">Dönüş</string>\n    <string name=\"patch_uninstall_text\">Farklı imzalar nedeniyle, yamalı uygulamayı yüklemeden önce orijinal uygulamayı kaldırmanız gerekir.\\nKişisel verilerinizi yedeklediğinizden emin olun.</string>\n    <string name=\"patch_install_successfully\">başarıyla yükle</string>\n    <string name=\"patch_install_failed\">Yükleme başarısız</string>\n    <string name=\"patch_no_xposed_module\">Xposed modül(ler)i bulunamadı</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Uygulamaları Seçin</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Ayarlar</string>\n    <string name=\"settings_keystore\">imza anahtar deposu</string>\n    <string name=\"settings_keystore_default\">yerleşik</string>\n    <string name=\"settings_keystore_custom\">Gelenek</string>\n    <string name=\"settings_keystore_dialog_title\">Özel anahtar deposu</string>\n    <string name=\"settings_keystore_file\">anahtar deposu dosyası</string>\n    <string name=\"settings_keystore_password\">Parola</string>\n    <string name=\"settings_keystore_alias\">takma ad</string>\n    <string name=\"settings_keystore_alias_password\">takma ad şifresi</string>\n    <string name=\"settings_keystore_desc\">Anahtar deposunu ayarla (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Yanlış türde anahtar deposu</string>\n    <string name=\"settings_keystore_wrong_password\">Yanlış anahtar deposu şifresi</string>\n    <string name=\"settings_keystore_wrong_alias\">Yanlış takma ad</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Yanlış takma ad şifresi</string>\n    <string name=\"settings_detail_patch_logs\">Ayrıntılı yama günlükleri</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Додати</string>\n    <string name=\"install\">Встановити</string>\n    <string name=\"installing\">Встановлення</string>\n    <string name=\"uninstall\">Видалити</string>\n    <string name=\"uninstalling\">Видалення</string>\n    <string name=\"copy_error\">Помилка копіювання</string>\n    <string name=\"apps\">Додатки</string>\n    <string name=\"modules\">Модулі</string>\n    <string name=\"shizuku_available\">Доступна послуга Shizuku</string>\n    <string name=\"shizuku_unavailable\">Служба Shizuku не підключена</string>\n    <string name=\"screen_repo\">Репо</string>\n    <string name=\"screen_logs\">Журнали</string>\n    <string name=\"off\">Вимкнено</string>\n    <string name=\"error_unknown\">Невідома помилка</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Деякі функції недоступні</string>\n    <string name=\"home_api_version\">Версія API</string>\n    <string name=\"home_lspatch_version\">Версія LSPatch</string>\n    <string name=\"home_framework_version\">Версія Framework</string>\n    <string name=\"home_system_version\">Версія системи</string>\n    <string name=\"home_device\">Пристрій</string>\n    <string name=\"home_system_abi\">Система ABI</string>\n    <string name=\"home_info_copied\">Скопійовано в буфер обміну</string>\n    <string name=\"home_support\">Підтримка</string>\n    <string name=\"home_description\">LSPatch — це безкоштовний некорневий фреймворк Xposed, заснований на ядрі LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Перегляньте вихідний код на %1$s<br/>Приєднуйтесь до нашого %2$s каналу]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Керування</string>\n    <string name=\"manage_loading\">Завантаження</string>\n    <string name=\"manage_no_apps\">Ще немає виправлених програм</string>\n    <string name=\"manage_rolling\">Прокатка</string>\n    <string name=\"manage_update_loader\">Оновити завантажувач</string>\n    <string name=\"manage_update_loader_successfully\">Оновлення завершено</string>\n    <string name=\"manage_update_loader_failed\">Оновлення не вдалося</string>\n    <string name=\"manage_module_scope\">Область застосування модуля</string>\n    <string name=\"manage_optimize\">Оптимізувати</string>\n    <string name=\"manage_optimize_successfully\">Оптимізація успішно</string>\n    <string name=\"manage_optimize_failed\">Не вдалося оптимізувати</string>\n    <string name=\"manage_uninstall_successfully\">Успішно видалити</string>\n    <string name=\"manage_no_modules\">Поки що відсутні модулі</string>\n    <string name=\"manage_module_settings\">Налаштування модуля</string>\n    <string name=\"manage_app_info\">Інформація про додаток</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Новий патч</string>\n    <string name=\"patch_select_dir_title\">Виберіть каталог зберігання</string>\n    <string name=\"patch_select_dir_text\">Виберіть каталог для зберігання виправлених apks</string>\n    <string name=\"patch_select_dir_error\">Помилка під час налаштування каталогу зберігання</string>\n    <string name=\"patch_from_storage\">Виберіть apk(s) зі сховища</string>\n    <string name=\"patch_from_applist\">Виберіть встановлену програму</string>\n    <string name=\"patch_mode\">Режим виправлення</string>\n    <string name=\"patch_local\">Місцеві</string>\n    <string name=\"patch_local_desc\">Виправлення програми без вбудованих модулів.\\nОбласть застосування можна динамічно змінювати без перевстановлення патчу.\\nЛокальні виправлені програми можна запускати лише на локальному пристрої.</string>\n    <string name=\"patch_integrated\">Інтегрований</string>\n    <string name=\"patch_integrated_desc\">Виправте додаток із вбудованими модулями.\\nВиправлена програма може працювати без диспетчера, але нею не можна керувати динамічно.\\nІнтегровані виправлені програми можна використовувати на пристроях, на яких не встановлено LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Вставляти модулі</string>\n    <string name=\"patch_debuggable\">Налагоджується</string>\n    <string name=\"patch_sigbypass\">Обхід підпису</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Вимкнено</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Обійти PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: обійти PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Замінити код версії</string>\n    <string name=\"patch_override_version_code_desc\">Замінити код версії виправленого додатка на 1\\nЦе дозволить встановити попередню версію в майбутньому, і, як правило, це не вплине на код версії, який фактично сприймається програмою</string>\n    <string name=\"patch_start\">Запустити патч</string>\n    <string name=\"patch_return\">Повернення</string>\n    <string name=\"patch_uninstall_text\">Через різні сигнатури вам потрібно видалити оригінальну програму, перш ніж встановлювати виправлену.\\nПереконайтеся, що ви створили резервну копію особистих даних.</string>\n    <string name=\"patch_install_successfully\">Встановити успішно</string>\n    <string name=\"patch_install_failed\">Помилка встановлення</string>\n    <string name=\"patch_no_xposed_module\">Не знайдено жодного модуля (модулів) Xposed</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Виберіть додатки</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Налаштування</string>\n    <string name=\"settings_keystore\">Сховище ключів підпису</string>\n    <string name=\"settings_keystore_default\">Вбудований</string>\n    <string name=\"settings_keystore_custom\">На замовлення</string>\n    <string name=\"settings_keystore_dialog_title\">Спеціальне сховище ключів</string>\n    <string name=\"settings_keystore_file\">Файл сховища ключів</string>\n    <string name=\"settings_keystore_password\">Пароль</string>\n    <string name=\"settings_keystore_alias\">Псевдонім</string>\n    <string name=\"settings_keystore_alias_password\">Псевдонім пароль</string>\n    <string name=\"settings_keystore_desc\">Встановити сховище ключів (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Неправильний тип сховища ключів</string>\n    <string name=\"settings_keystore_wrong_password\">Неправильний пароль сховища ключів</string>\n    <string name=\"settings_keystore_wrong_alias\">Неправильний псевдонім</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Неправильний пароль псевдоніма</string>\n    <string name=\"settings_detail_patch_logs\">Детальні журнали патчів</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-ur/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">شامل کریں۔ </string>\n    <string name=\"install\">Install</string>\n    <string name=\"installing\">Installال</string>\n    <string name=\"uninstall\">ان انسٹال کریں۔</string>\n    <string name=\"uninstalling\">ان انسٹال کرنا</string>\n    <string name=\"copy_error\">کاپی کی غلطی</string>\n    <string name=\"apps\">ایپس</string>\n    <string name=\"modules\">ماڈیولز</string>\n    <string name=\"shizuku_available\">شیزوکو سروس دستیاب ہے۔</string>\n    <string name=\"shizuku_unavailable\">شیزوکو سروس منسلک نہیں ہے۔</string>\n    <string name=\"screen_repo\">ریپو</string>\n    <string name=\"screen_logs\">نوشتہ جات</string>\n    <string name=\"off\">بند</string>\n    <string name=\"error_unknown\">نامعلوم خامی</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">کچھ فنکشنز دستیاب نہیں ہیں۔</string>\n    <string name=\"home_api_version\">API ورژن</string>\n    <string name=\"home_lspatch_version\">ایل ایس پیچ ورژن</string>\n    <string name=\"home_framework_version\">فریم ورک ورژن</string>\n    <string name=\"home_system_version\">سسٹم ورژن</string>\n    <string name=\"home_device\">ڈیوائس</string>\n    <string name=\"home_system_abi\">سسٹم ABI</string>\n    <string name=\"home_info_copied\">کلپ بورڈ پر کاپی ہو گیا۔</string>\n    <string name=\"home_support\">حمایت</string>\n    <string name=\"home_description\">LSPatch LSPosed کور پر مبنی ایک مفت نان روٹ Xposed فریم ورک ہے۔</string>\n    <string name=\"home_view_source_code\"><![CDATA[%1$s<br/>پر سورس کوڈ دیکھیں ہمارے %2$s چینل میں شامل ہوں۔]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">انتظام کریں۔</string>\n    <string name=\"manage_loading\">لوڈ ہو رہا ہے۔</string>\n    <string name=\"manage_no_apps\">ابھی تک کوئی پیچ شدہ ایپس نہیں ہیں۔</string>\n    <string name=\"manage_rolling\">رولنگ</string>\n    <string name=\"manage_update_loader\">لوڈر کو اپ ڈیٹ کریں۔</string>\n    <string name=\"manage_update_loader_successfully\">کامیابی سے اپ ڈیٹ ہو گیا۔</string>\n    <string name=\"manage_update_loader_failed\">اپ ڈیٹ ناکام</string>\n    <string name=\"manage_module_scope\">ماڈیول دائرہ کار</string>\n    <string name=\"manage_optimize\">بہتر بنائیں</string>\n    <string name=\"manage_optimize_successfully\">کامیابی کے ساتھ اصلاح کریں۔</string>\n    <string name=\"manage_optimize_failed\">اصلاح ناکام ہوگئی</string>\n    <string name=\"manage_uninstall_successfully\">کامیابی کے ساتھ اَن انسٹال کریں۔</string>\n    <string name=\"manage_no_modules\">ابھی تک کوئی ماڈیولز نہیں ہیں۔</string>\n    <string name=\"manage_module_settings\">ماڈیول کی ترتیبات</string>\n    <string name=\"manage_app_info\">ایپ کی معلومات</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">نیا پیچ</string>\n    <string name=\"patch_select_dir_title\">اسٹوریج ڈائرکٹری منتخب کریں۔</string>\n    <string name=\"patch_select_dir_text\">پیچ شدہ apks کو ذخیرہ کرنے کے لیے ایک ڈائریکٹری منتخب کریں۔</string>\n    <string name=\"patch_select_dir_error\">اسٹوریج ڈائرکٹری ترتیب دیتے وقت خرابی</string>\n    <string name=\"patch_from_storage\">اسٹوریج سے apk(s) کو منتخب کریں۔</string>\n    <string name=\"patch_from_applist\">انسٹال کردہ ایپ منتخب کریں۔</string>\n    <string name=\"patch_mode\">پیچ موڈ</string>\n    <string name=\"patch_local\">مقامی</string>\n    <string name=\"patch_local_desc\">ایمبیڈڈ ماڈیولز کے بغیر ایپ کو پیچ کریں۔\\nایکس پوزڈ اسکوپ کو دوبارہ پیچ کے بغیر متحرک طور پر تبدیل کیا جا سکتا ہے۔\\nمقامی پیچ والی ایپس صرف مقامی ڈیوائس پر چل سکتی ہیں۔</string>\n    <string name=\"patch_integrated\">ضم</string>\n    <string name=\"patch_integrated_desc\">ایمبیڈڈ ماڈیولز کے ساتھ ایک ایپ پیچ کریں۔\\nپیچ شدہ ایپ مینیجر کے بغیر چل سکتی ہے، لیکن اسے متحرک طور پر منظم نہیں کیا جا سکتا۔\\nانٹیگریٹڈ پیچ شدہ ایپس ان آلات پر استعمال کی جا سکتی ہیں جن میں LSPatch مینیجر انسٹال نہیں ہے۔</string>\n    <string name=\"patch_embed_modules\">ایمبیڈ ماڈیولز</string>\n    <string name=\"patch_debuggable\">ڈیبگ ایبل</string>\n    <string name=\"patch_sigbypass\">دستخط بائی پاس</string>\n    <string name=\"patch_sigbypasslv0\">lv0: آف</string>\n    <string name=\"patch_sigbypasslv1\">lv1: PM کو بائی پاس کریں۔</string>\n    <string name=\"patch_sigbypasslv2\">lv2: بائی پاس PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">ورژن کوڈ کو اوور رائڈ کریں۔</string>\n    <string name=\"patch_override_version_code_desc\">پیچ شدہ ایپ کے ورژن کوڈ کو 1\\nپر اوور رائیڈ کریں یہ مستقبل میں انسٹالیشن کو ڈاؤن گریڈ کرنے کی اجازت دیتا ہے، اور عام طور پر اس سے ایپلیکیشن کے ذریعہ سمجھے گئے ورژن کوڈ پر کوئی اثر نہیں پڑے گا۔</string>\n    <string name=\"patch_start\">پیچ شروع کریں۔</string>\n    <string name=\"patch_return\">واپسی</string>\n    <string name=\"patch_uninstall_text\">مختلف دستخطوں کی وجہ سے، آپ کو پیچ شدہ ایپ کو انسٹال کرنے سے پہلے اصل ایپ کو ان انسٹال کرنے کی ضرورت ہے۔\\nیقینی بنائیں کہ آپ نے ذاتی ڈیٹا کا بیک اپ لیا ہے۔</string>\n    <string name=\"patch_install_successfully\">کامیابی سے انسٹال کریں۔</string>\n    <string name=\"patch_install_failed\">انسٹال ناکام ہو گیا۔</string>\n    <string name=\"patch_no_xposed_module\">کوئی ایکس پوز ماڈیول نہیں ملا</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">ایپس کو منتخب کریں۔</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">ترتیبات</string>\n    <string name=\"settings_keystore\">دستخطی کی اسٹور</string>\n    <string name=\"settings_keystore_default\">بلٹ ان</string>\n    <string name=\"settings_keystore_custom\">اپنی مرضی کے مطابق</string>\n    <string name=\"settings_keystore_dialog_title\">حسب ضرورت کی اسٹور</string>\n    <string name=\"settings_keystore_file\">کلیدی اسٹور فائل</string>\n    <string name=\"settings_keystore_password\">پاس ورڈ</string>\n    <string name=\"settings_keystore_alias\">عرف</string>\n    <string name=\"settings_keystore_alias_password\">عرفی پاس ورڈ</string>\n    <string name=\"settings_keystore_desc\">سیٹ کی اسٹور (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">کی اسٹور کی غلط قسم</string>\n    <string name=\"settings_keystore_wrong_password\">کی اسٹور کا غلط پاس ورڈ</string>\n    <string name=\"settings_keystore_wrong_alias\">غلط عرفی نام</string>\n    <string name=\"settings_keystore_wrong_alias_password\">غلط عرفی پاس ورڈ</string>\n    <string name=\"settings_detail_patch_logs\">تفصیلی پیچ لاگز</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Thêm vào</string>\n    <string name=\"install\">Cài đặt</string>\n    <string name=\"installing\">Đang cài đặt</string>\n    <string name=\"uninstall\">Gỡ cài đặt</string>\n    <string name=\"uninstalling\">Gỡ cài đặt</string>\n    <string name=\"copy_error\">Sao chép lỗi</string>\n    <string name=\"apps\">Ứng dụng</string>\n    <string name=\"modules\">Mô-đun</string>\n    <string name=\"shizuku_available\">Có dịch vụ Shizuku</string>\n    <string name=\"shizuku_unavailable\">Dịch vụ Shizuku không được kết nối</string>\n    <string name=\"screen_repo\">Repo</string>\n    <string name=\"screen_logs\">Nhật ký</string>\n    <string name=\"off\">Tắt</string>\n    <string name=\"error_unknown\">Lỗi không rõ</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">Một số chức năng không khả dụng</string>\n    <string name=\"home_api_version\">Phiên bản API</string>\n    <string name=\"home_lspatch_version\">LSPatch Version</string>\n    <string name=\"home_framework_version\">Phiên bản khung</string>\n    <string name=\"home_system_version\">Phiên bản hệ thống</string>\n    <string name=\"home_device\">Thiết bị</string>\n    <string name=\"home_system_abi\">Hệ thống ABI</string>\n    <string name=\"home_info_copied\">Sao chép vào clipboard</string>\n    <string name=\"home_support\">Hỗ trợ</string>\n    <string name=\"home_description\">LSPatch là một khung công tác Xposed không gốc miễn phí dựa trên lõi LSPosed.</string>\n    <string name=\"home_view_source_code\"><![CDATA[Xem mã nguồn tại %1$s<br/>Tham gia kênh %2$s của chúng tôi]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">Quản lý</string>\n    <string name=\"manage_loading\">Đang tải</string>\n    <string name=\"manage_no_apps\">Chưa có ứng dụng được vá</string>\n    <string name=\"manage_rolling\">Lăn</string>\n    <string name=\"manage_update_loader\">Cập nhật trình tải</string>\n    <string name=\"manage_update_loader_successfully\">Cập nhật thành công</string>\n    <string name=\"manage_update_loader_failed\">Cập nhật không thành công</string>\n    <string name=\"manage_module_scope\">Phạm vi mô-đun</string>\n    <string name=\"manage_optimize\">Tối ưu hóa</string>\n    <string name=\"manage_optimize_successfully\">Tối ưu hóa thành công</string>\n    <string name=\"manage_optimize_failed\">Tối ưu hóa không thành công</string>\n    <string name=\"manage_uninstall_successfully\">Gỡ cài đặt thành công</string>\n    <string name=\"manage_no_modules\">Chưa có mô-đun nào</string>\n    <string name=\"manage_module_settings\">Cài đặt mô-đun</string>\n    <string name=\"manage_app_info\">Thông tin ứng dụng</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">Bản vá mới</string>\n    <string name=\"patch_select_dir_title\">Chọn thư mục lưu trữ</string>\n    <string name=\"patch_select_dir_text\">Chọn một thư mục để lưu trữ các bản vá lỗi</string>\n    <string name=\"patch_select_dir_error\">Lỗi khi đặt thư mục lưu trữ</string>\n    <string name=\"patch_from_storage\">Chọn (các) gói ứng dụng từ bộ nhớ</string>\n    <string name=\"patch_from_applist\">Chọn một ứng dụng đã cài đặt</string>\n    <string name=\"patch_mode\">Chế độ vá lỗi</string>\n    <string name=\"patch_local\">Địa phương</string>\n    <string name=\"patch_local_desc\">Vá một ứng dụng không có mô-đun được nhúng.\\nXpose có thể được thay đổi linh hoạt mà không cần vá lại.\\nCác ứng dụng được vá cục bộ chỉ có thể chạy trên thiết bị cục bộ.</string>\n    <string name=\"patch_integrated\">tích hợp</string>\n    <string name=\"patch_integrated_desc\">Vá một ứng dụng với các mô-đun được nhúng.\\nỨng dụng đã vá có thể chạy mà không cần trình quản lý, nhưng không thể quản lý động.\\nCác ứng dụng được vá tích hợp có thể được sử dụng trên các thiết bị chưa cài đặt LSPatch Manager.</string>\n    <string name=\"patch_embed_modules\">Nhúng mô-đun</string>\n    <string name=\"patch_debuggable\">Có thể gỡ lỗi</string>\n    <string name=\"patch_sigbypass\">Bỏ qua chữ ký</string>\n    <string name=\"patch_sigbypasslv0\">lv0: Tắt</string>\n    <string name=\"patch_sigbypasslv1\">lv1: Bỏ qua PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: Bỏ qua PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">Ghi đè mã phiên bản</string>\n    <string name=\"patch_override_version_code_desc\">Ghi đè mã phiên bản của ứng dụng đã vá thành 1\\nĐiều này cho phép hạ cấp cài đặt trong tương lai và nói chung điều này sẽ không ảnh hưởng đến mã phiên bản mà ứng dụng thực sự nhận thấy</string>\n    <string name=\"patch_start\">Bắt đầu bản vá</string>\n    <string name=\"patch_return\">Trở về</string>\n    <string name=\"patch_uninstall_text\">Do các chữ ký khác nhau, bạn cần gỡ cài đặt ứng dụng gốc trước khi cài đặt bản vá.\\nĐảm bảo rằng bạn đã sao lưu dữ liệu cá nhân.</string>\n    <string name=\"patch_install_successfully\">Cài đặt thành công</string>\n    <string name=\"patch_install_failed\">Cài đặt không thành công</string>\n    <string name=\"patch_no_xposed_module\">Không tìm thấy (các) mô-đun Xpose nào</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">Chọn ứng dụng</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">Cài đặt</string>\n    <string name=\"settings_keystore\">Kho khóa chữ ký</string>\n    <string name=\"settings_keystore_default\">Được xây dựng trong</string>\n    <string name=\"settings_keystore_custom\">Tập quán</string>\n    <string name=\"settings_keystore_dialog_title\">Kho khóa tùy chỉnh</string>\n    <string name=\"settings_keystore_file\">Tệp kho khóa</string>\n    <string name=\"settings_keystore_password\">Mật khẩu</string>\n    <string name=\"settings_keystore_alias\">Bí danh</string>\n    <string name=\"settings_keystore_alias_password\">Mật khẩu bí danh</string>\n    <string name=\"settings_keystore_desc\">Đặt kho khóa (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">Kho khóa sai loại</string>\n    <string name=\"settings_keystore_wrong_password\">Mật khẩu kho khóa sai</string>\n    <string name=\"settings_keystore_wrong_alias\">Tên bí danh sai</string>\n    <string name=\"settings_keystore_wrong_alias_password\">Mật khẩu bí danh sai</string>\n    <string name=\"settings_detail_patch_logs\">Nhật ký vá chi tiết</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">添加</string>\n    <string name=\"install\">安装</string>\n    <string name=\"installing\">安装中</string>\n    <string name=\"uninstall\">卸载</string>\n    <string name=\"uninstalling\">卸载中</string>\n    <string name=\"copy_error\">复制错误信息</string>\n    <string name=\"apps\">应用</string>\n    <string name=\"modules\">模块</string>\n    <string name=\"shizuku_available\">Shizuku 服务可用</string>\n    <string name=\"shizuku_unavailable\">Shizuku 服务未连接</string>\n    <string name=\"screen_repo\">仓库</string>\n    <string name=\"screen_logs\">日志</string>\n    <string name=\"off\">关闭</string>\n    <string name=\"error_unknown\">未知错误</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">部分功能不可用</string>\n    <string name=\"home_api_version\">API 版本</string>\n    <string name=\"home_lspatch_version\">LSPatch 版本</string>\n    <string name=\"home_framework_version\">框架版本</string>\n    <string name=\"home_system_version\">系统版本</string>\n    <string name=\"home_device\">设备</string>\n    <string name=\"home_system_abi\">系统架构</string>\n    <string name=\"home_info_copied\">已复制到剪贴板</string>\n    <string name=\"home_support\">支持</string>\n    <string name=\"home_description\">LSPatch 是一款免费的基于 LSPosed 核心的免 Root Xposed 框架。</string>\n    <string name=\"home_view_source_code\"><![CDATA[查看源代码 %1$s<br/>加入我们的 %2$s 频道]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">管理</string>\n    <string name=\"manage_loading\">加载中</string>\n    <string name=\"manage_no_apps\">尚无已修补的应用</string>\n    <string name=\"manage_rolling\">滚动</string>\n    <string name=\"manage_update_loader\">更新加载器</string>\n    <string name=\"manage_update_loader_successfully\">更新成功</string>\n    <string name=\"manage_update_loader_failed\">更新失败</string>\n    <string name=\"manage_module_scope\">模块作用域</string>\n    <string name=\"manage_optimize\">优化</string>\n    <string name=\"manage_optimize_successfully\">优化成功</string>\n    <string name=\"manage_optimize_failed\">优化失败</string>\n    <string name=\"manage_uninstall_successfully\">卸载成功</string>\n    <string name=\"manage_no_modules\">尚无模块</string>\n    <string name=\"manage_module_settings\">模块设置</string>\n    <string name=\"manage_app_info\">应用信息</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">新建修补</string>\n    <string name=\"patch_select_dir_title\">选择存储目录</string>\n    <string name=\"patch_select_dir_text\">选择一个目录来存储已修补的 apk</string>\n    <string name=\"patch_select_dir_error\">设置存储目录时出错</string>\n    <string name=\"patch_from_storage\">从存储目录中选择（多个）apk</string>\n    <string name=\"patch_from_applist\">选择已安装的应用程序</string>\n    <string name=\"patch_mode\">修补模式</string>\n    <string name=\"patch_local\">本地模式</string>\n    <string name=\"patch_local_desc\">为未嵌入模块的应用程序打补丁。\\nXposed 范围可动态更改，无需重新打补丁。\\n打了本地补丁的应用程序只能在本地设备上运行。</string>\n    <string name=\"patch_integrated\">集成模式</string>\n    <string name=\"patch_integrated_desc\">修补 App 并内置模块。\\n经修补的应用可以在没有管理器的情况下运行，但不能动态管理配置。\\n以集成模式修补的应用可在未安装 LSPatch 管理器的设备上运行。</string>\n    <string name=\"patch_embed_modules\">嵌入模块</string>\n    <string name=\"patch_debuggable\">可调试</string>\n    <string name=\"patch_sigbypass\">破解签名校验</string>\n    <string name=\"patch_sigbypasslv0\">lv0: 关闭</string>\n    <string name=\"patch_sigbypasslv1\">lv1: 绕过 PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: 绕过 PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">覆写版本号</string>\n    <string name=\"patch_override_version_code_desc\">将修补的 App 版本号重写为 1\\n这将允许后续降级安装，并且通常来说这不会影响应用实际感知到的版本号</string>\n    <string name=\"patch_start\">开始修补</string>\n    <string name=\"patch_return\">返回</string>\n    <string name=\"patch_uninstall_text\">由于签名不同，安装修补的应用前需要先卸载原应用。\\n确保您已备份好个人数据。</string>\n    <string name=\"patch_install_successfully\">安装成功</string>\n    <string name=\"patch_install_failed\">安装失败</string>\n    <string name=\"patch_no_xposed_module\">未找到 Xposed 模块</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">选择应用程序</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">设置</string>\n    <string name=\"settings_keystore\">签名密钥库</string>\n    <string name=\"settings_keystore_default\">内置</string>\n    <string name=\"settings_keystore_custom\">自定义</string>\n    <string name=\"settings_keystore_dialog_title\">自定义密钥库</string>\n    <string name=\"settings_keystore_file\">密钥库文件</string>\n    <string name=\"settings_keystore_password\">密码</string>\n    <string name=\"settings_keystore_alias\">别名</string>\n    <string name=\"settings_keystore_alias_password\">别名密码</string>\n    <string name=\"settings_keystore_desc\">设置密钥库（BKS）</string>\n    <string name=\"settings_keystore_wrong_keystore\">密钥库类型错误</string>\n    <string name=\"settings_keystore_wrong_password\">密钥库密码错误</string>\n    <string name=\"settings_keystore_wrong_alias\">别名错误</string>\n    <string name=\"settings_keystore_wrong_alias_password\">别名密码错误</string>\n    <string name=\"settings_detail_patch_logs\">详细修补日志</string>\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">Add</string>\n    <string name=\"install\">安裝</string>\n    <string name=\"installing\">安裝中</string>\n    <string name=\"uninstall\">卸载</string>\n    <string name=\"uninstalling\"></string>\n    <string name=\"shizuku_available\">Shizuku 服務可用</string>\n    <string name=\"shizuku_unavailable\">Shizuku 服務未連接</string>\n    <string name=\"screen_logs\">日誌</string>\n    <!-- Home Screen -->\n    <!-- Manage Screen -->\n    <!-- New Patch Screen -->\n    <!-- Select Apps Screen -->\n    <!-- Settings Screen -->\n</resources>\n"
  },
  {
    "path": "manager/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"add\">新增</string>\n    <string name=\"install\">安裝</string>\n    <string name=\"installing\">安裝中</string>\n    <string name=\"uninstall\">解除安裝</string>\n    <string name=\"uninstalling\">解除安裝中</string>\n    <string name=\"copy_error\">複製錯誤</string>\n    <string name=\"apps\">應用程式</string>\n    <string name=\"modules\">模組</string>\n    <string name=\"shizuku_available\">Shizuku 服務可用</string>\n    <string name=\"shizuku_unavailable\">Shizuku 服務未連線</string>\n    <string name=\"screen_repo\">倉庫</string>\n    <string name=\"screen_logs\">日誌</string>\n    <string name=\"off\">關閉</string>\n    <string name=\"error_unknown\">未知錯誤</string>\n    <!-- Home Screen -->\n    <string name=\"home_shizuku_warning\">部分功能不可用</string>\n    <string name=\"home_api_version\">API 版本</string>\n    <string name=\"home_lspatch_version\">LSPatch 版本</string>\n    <string name=\"home_framework_version\">框架版本</string>\n    <string name=\"home_system_version\">系統版本</string>\n    <string name=\"home_device\">裝置</string>\n    <string name=\"home_system_abi\">系統架構</string>\n    <string name=\"home_info_copied\">已複製到剪貼簿</string>\n    <string name=\"home_support\">支援</string>\n    <string name=\"home_description\">LSPatch 是一款免費的基於 LSPosed 核心的免 Root Xposed 框架。</string>\n    <string name=\"home_view_source_code\"><![CDATA[查看原始碼 %1$s<br/>加入我們的 %2$s 頻道]]></string>\n    <!-- Manage Screen -->\n    <string name=\"screen_manage\">管理</string>\n    <string name=\"manage_loading\">正在加載</string>\n    <string name=\"manage_no_apps\">還沒有打包的應用程式</string>\n    <string name=\"manage_rolling\">滾動</string>\n    <string name=\"manage_update_loader\">更新載入程式</string>\n    <string name=\"manage_update_loader_successfully\">更新成功</string>\n    <string name=\"manage_update_loader_failed\">更新失敗</string>\n    <string name=\"manage_module_scope\">模組作用域</string>\n    <string name=\"manage_optimize\">最佳化</string>\n    <string name=\"manage_optimize_successfully\">最佳化成功</string>\n    <string name=\"manage_optimize_failed\">最佳化失敗</string>\n    <string name=\"manage_uninstall_successfully\">解除安裝成功</string>\n    <string name=\"manage_no_modules\">還沒有模組</string>\n    <string name=\"manage_module_settings\">模組設定</string>\n    <string name=\"manage_app_info\">程式資訊</string>\n    <!-- New Patch Screen -->\n    <string name=\"screen_new_patch\">新增打包程式</string>\n    <string name=\"patch_select_dir_title\">選擇儲存資料夾</string>\n    <string name=\"patch_select_dir_text\">選擇一個資料夾儲存打包好的 apk</string>\n    <string name=\"patch_select_dir_error\">設定儲存資料夾出錯</string>\n    <string name=\"patch_from_storage\">從儲存空間中選取 apk</string>\n    <string name=\"patch_from_applist\">選取已安裝的應用程式</string>\n    <string name=\"patch_mode\">打包模式</string>\n    <string name=\"patch_local\">本機</string>\n    <string name=\"patch_local_desc\">修補未嵌入模組的應用程式。\\nXpose 範圍可以動態更改，無需重新修補。\\n本地修補的應用程式只能在本機裝置上運作。</string>\n    <string name=\"patch_integrated\">集成模式</string>\n    <string name=\"patch_integrated_desc\">打包內建模組的應用程式。\\n打包後的程式，可以在沒有管理器的情況下執行，但無法動態管理啟用的模組。\\n以集成模式打包的應用程式，可以在沒有安裝 LSPatch Manager 的裝置上使用。</string>\n    <string name=\"patch_embed_modules\">內建模組</string>\n    <string name=\"patch_debuggable\">程式可偵錯</string>\n    <string name=\"patch_sigbypass\">破解簽名驗證</string>\n    <string name=\"patch_sigbypasslv0\">lv0: 關閉</string>\n    <string name=\"patch_sigbypasslv1\">lv1: 繞過 PM</string>\n    <string name=\"patch_sigbypasslv2\">lv2: 繞過 PM + openat (libc)</string>\n    <string name=\"patch_override_version_code\">覆蓋版本編號</string>\n    <string name=\"patch_override_version_code_desc\">將打包應用程式的版本編號改成 1\\n允許以後降級安裝，一般來說，這不會影響應用程式實際感知的版本編號。</string>\n    <string name=\"patch_start\">開始打包</string>\n    <string name=\"patch_return\">返回</string>\n    <string name=\"patch_uninstall_text\">由於簽名不同，安裝前需要先解除安裝原程式。\\n確保您已備份好個人資料。</string>\n    <string name=\"patch_install_successfully\">安裝成功</string>\n    <string name=\"patch_install_failed\">安裝失敗</string>\n    <string name=\"patch_no_xposed_module\">未找到 Xpose 模組</string>\n    <!-- Select Apps Screen -->\n    <string name=\"screen_select_apps\">選取應用程式</string>\n    <!-- Settings Screen -->\n    <string name=\"screen_settings\">設定</string>\n    <string name=\"settings_keystore\">簽名金鑰庫</string>\n    <string name=\"settings_keystore_default\">內建</string>\n    <string name=\"settings_keystore_custom\">自訂</string>\n    <string name=\"settings_keystore_dialog_title\">自訂金鑰庫</string>\n    <string name=\"settings_keystore_file\">金鑰庫檔案</string>\n    <string name=\"settings_keystore_password\">密碼</string>\n    <string name=\"settings_keystore_alias\">別名</string>\n    <string name=\"settings_keystore_alias_password\">別名密碼</string>\n    <string name=\"settings_keystore_desc\">設定金鑰庫 (BKS)</string>\n    <string name=\"settings_keystore_wrong_keystore\">金鑰庫類型錯誤</string>\n    <string name=\"settings_keystore_wrong_password\">金鑰庫密碼錯誤</string>\n    <string name=\"settings_keystore_wrong_alias\">別名錯誤</string>\n    <string name=\"settings_keystore_wrong_alias_password\">別名密碼錯誤</string>\n    <string name=\"settings_detail_patch_logs\">詳細打包日誌</string>\n</resources>\n"
  },
  {
    "path": "meta-loader/.gitignore",
    "content": "/build"
  },
  {
    "path": "meta-loader/build.gradle.kts",
    "content": "import java.util.Locale\n\nplugins {\n    alias(libs.plugins.agp.app)\n}\n\nandroid {\n    defaultConfig {\n        multiDexEnabled = false\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            proguardFiles(\"proguard-rules.pro\")\n        }\n    }\n    namespace = \"org.lsposed.lspatch.metaloader\"\n}\n\nandroidComponents.onVariants { variant ->\n    val variantCapped = variant.name.replaceFirstChar { it.uppercase() }\n    val variantLowered = variant.name.lowercase()\n\n    task<Copy>(\"copyDex$variantCapped\") {\n        dependsOn(\"assemble$variantCapped\")\n        val dexOutPath = if (variant.buildType == \"release\")\n            \"$buildDir/intermediates/dex/$variantLowered/minify${variantCapped}WithR8\" else\n            \"$buildDir/intermediates/dex/$variantLowered/mergeDex$variantCapped\"\n        from(dexOutPath)\n        rename(\"classes.dex\", \"metaloader.dex\")\n        into(\"${rootProject.projectDir}/out/assets/${variant.name}/lspatch\")\n    }\n\n    task(\"copy$variantCapped\") {\n        dependsOn(\"copyDex$variantCapped\")\n\n        doLast {\n            println(\"Loader dex has been copied to ${rootProject.projectDir}${File.separator}out\")\n        }\n    }\n}\n\ndependencies {\n    compileOnly(projects.hiddenapi.stubs)\n    implementation(projects.share.java)\n    implementation(libs.hiddenapibypass)\n}\n"
  },
  {
    "path": "meta-loader/proguard-rules.pro",
    "content": "-keep class org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub {\n    public static byte[] dex;\n    <init>();\n}\n-dontwarn androidx.annotation.NonNull\n-dontwarn androidx.annotation.Nullable\n-dontwarn androidx.annotation.VisibleForTesting\n"
  },
  {
    "path": "meta-loader/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "meta-loader/src/main/java/org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub.java",
    "content": "package org.lsposed.lspatch.metaloader;\n\nimport android.annotation.SuppressLint;\nimport android.app.AppComponentFactory;\nimport android.content.pm.ApplicationInfo;\nimport android.content.pm.IPackageManager;\nimport android.os.Build;\nimport android.os.Process;\nimport android.os.ServiceManager;\nimport android.util.JsonReader;\nimport android.util.Log;\n\nimport org.lsposed.hiddenapibypass.HiddenApiBypass;\nimport org.lsposed.lspatch.share.Constants;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.lang.reflect.Method;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.zip.ZipFile;\n\n@SuppressLint(\"UnsafeDynamicallyLoadedCode\")\npublic class LSPAppComponentFactoryStub extends AppComponentFactory {\n\n    private static final String TAG = \"LSPatch-MetaLoader\";\n    private static final Map<String, String> archToLib = new HashMap<String, String>(4);\n\n    public static byte[] dex;\n\n    static {\n        try {\n            archToLib.put(\"arm\", \"armeabi-v7a\");\n            archToLib.put(\"arm64\", \"arm64-v8a\");\n            archToLib.put(\"x86\", \"x86\");\n            archToLib.put(\"x86_64\", \"x86_64\");\n\n            var cl = Objects.requireNonNull(LSPAppComponentFactoryStub.class.getClassLoader());\n            Class<?> VMRuntime = Class.forName(\"dalvik.system.VMRuntime\");\n            Method getRuntime = VMRuntime.getDeclaredMethod(\"getRuntime\");\n            getRuntime.setAccessible(true);\n            Method vmInstructionSet = VMRuntime.getDeclaredMethod(\"vmInstructionSet\");\n            vmInstructionSet.setAccessible(true);\n            String arch = (String) vmInstructionSet.invoke(getRuntime.invoke(null));\n            String libName = archToLib.get(arch);\n\n            boolean useManager = false;\n            String soPath;\n\n            try (var is = cl.getResourceAsStream(Constants.CONFIG_ASSET_PATH);\n                 var reader = new JsonReader(new InputStreamReader(is))) {\n                reader.beginObject();\n                while (reader.hasNext()) {\n                    var name = reader.nextName();\n                    if (name.equals(\"useManager\")) {\n                        useManager = reader.nextBoolean();\n                        break;\n                    } else {\n                        reader.skipValue();\n                    }\n                }\n            }\n\n            if (useManager) {\n                Log.i(TAG, \"Bootstrap loader from manager\");\n                var ipm = IPackageManager.Stub.asInterface(ServiceManager.getService(\"package\"));\n                ApplicationInfo manager;\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    manager = (ApplicationInfo) HiddenApiBypass.invoke(IPackageManager.class, ipm, \"getApplicationInfo\", Constants.MANAGER_PACKAGE_NAME, 0L, Process.myUid() / 100000);\n                } else {\n                    manager = ipm.getApplicationInfo(Constants.MANAGER_PACKAGE_NAME, 0, Process.myUid() / 100000);\n                }\n                try (var zip = new ZipFile(new File(manager.sourceDir));\n                     var is = zip.getInputStream(zip.getEntry(Constants.LOADER_DEX_ASSET_PATH));\n                     var os = new ByteArrayOutputStream()) {\n                    transfer(is, os);\n                    dex = os.toByteArray();\n                }\n                soPath = manager.sourceDir + \"!/assets/lspatch/so/\" + libName + \"/liblspatch.so\";\n            } else {\n                Log.i(TAG, \"Bootstrap loader from embedment\");\n                try (var is = cl.getResourceAsStream(Constants.LOADER_DEX_ASSET_PATH);\n                     var os = new ByteArrayOutputStream()) {\n                    transfer(is, os);\n                    dex = os.toByteArray();\n                }\n                soPath = cl.getResource(\"assets/lspatch/so/\" + libName + \"/liblspatch.so\").getPath().substring(5);\n            }\n\n            System.load(soPath);\n        } catch (Throwable e) {\n            throw new ExceptionInInitializerError(e);\n        }\n    }\n\n    private static void transfer(InputStream is, OutputStream os) throws IOException {\n        byte[] buffer = new byte[8192];\n        int n;\n        while (-1 != (n = is.read(buffer))) {\n            os.write(buffer, 0, n);\n        }\n    }\n}\n"
  },
  {
    "path": "patch/.gitignore",
    "content": "/build"
  },
  {
    "path": "patch/build.gradle.kts",
    "content": "val androidSourceCompatibility: JavaVersion by rootProject.extra\nval androidTargetCompatibility: JavaVersion by rootProject.extra\n\nplugins {\n    id(\"java-library\")\n}\n\njava {\n    sourceCompatibility = androidSourceCompatibility\n    targetCompatibility = androidTargetCompatibility\n    sourceSets {\n        main {\n            java.srcDirs(\"libs/manifest-editor/lib/src/main/java\")\n            resources.srcDirs(\"libs/manifest-editor/lib/src/main\")\n        }\n    }\n}\n\ndependencies {\n    implementation(projects.apkzlib)\n    implementation(projects.share.java)\n\n    implementation(lspatch.commons.io)\n    implementation(lspatch.beust.jcommander)\n    implementation(lspatch.google.gson)\n}\n"
  },
  {
    "path": "patch/src/main/java/org/lsposed/patch/LSPatch.java",
    "content": "package org.lsposed.patch;\n\nimport static org.lsposed.lspatch.share.Constants.CONFIG_ASSET_PATH;\nimport static org.lsposed.lspatch.share.Constants.EMBEDDED_MODULES_ASSET_PATH;\nimport static org.lsposed.lspatch.share.Constants.LOADER_DEX_ASSET_PATH;\nimport static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH;\nimport static org.lsposed.lspatch.share.Constants.PROXY_APP_COMPONENT_FACTORY;\n\nimport com.android.tools.build.apkzlib.sign.SigningExtension;\nimport com.android.tools.build.apkzlib.sign.SigningOptions;\nimport com.android.tools.build.apkzlib.zip.AlignmentRules;\nimport com.android.tools.build.apkzlib.zip.StoredEntry;\nimport com.android.tools.build.apkzlib.zip.ZFile;\nimport com.android.tools.build.apkzlib.zip.ZFileOptions;\nimport com.beust.jcommander.JCommander;\nimport com.beust.jcommander.Parameter;\nimport com.beust.jcommander.ParameterException;\nimport com.google.gson.Gson;\nimport com.wind.meditor.core.ManifestEditor;\nimport com.wind.meditor.property.AttributeItem;\nimport com.wind.meditor.property.ModificationProperty;\nimport com.wind.meditor.utils.NodeValue;\n\nimport org.apache.commons.io.FilenameUtils;\nimport org.lsposed.lspatch.share.Constants;\nimport org.lsposed.lspatch.share.LSPConfig;\nimport org.lsposed.lspatch.share.PatchConfig;\nimport org.lsposed.patch.util.ApkSignatureHelper;\nimport org.lsposed.patch.util.JavaLogger;\nimport org.lsposed.patch.util.Logger;\nimport org.lsposed.patch.util.ManifestParser;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.security.KeyStore;\nimport java.security.cert.X509Certificate;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Base64;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.Set;\n\npublic class LSPatch {\n\n    static class PatchError extends Error {\n        public PatchError(String message, Throwable cause) {\n            super(message, cause);\n        }\n\n        PatchError(String message) {\n            super(message);\n        }\n    }\n\n    @Parameter(description = \"apks\")\n    private List<String> apkPaths = new ArrayList<>();\n\n    @Parameter(names = {\"-h\", \"--help\"}, help = true, order = 0, description = \"Print this message\")\n    private boolean help = false;\n\n    @Parameter(names = {\"-o\", \"--output\"}, description = \"Output directory\")\n    private String outputPath = \".\";\n\n    @Parameter(names = {\"-f\", \"--force\"}, description = \"Force overwrite exists output file\")\n    private boolean forceOverwrite = false;\n\n    @Parameter(names = {\"-d\", \"--debuggable\"}, description = \"Set app to be debuggable\")\n    private boolean debuggableFlag = false;\n\n    @Parameter(names = {\"-l\", \"--sigbypasslv\"}, description = \"Signature bypass level. 0 (disable), 1 (pm), 2 (pm+openat). default 0\")\n    private int sigbypassLevel = 0;\n\n    @Parameter(names = {\"-k\", \"--keystore\"}, arity = 4, description = \"Set custom signature keystore. Followed by 4 arguments: keystore path, keystore password, keystore alias, keystore alias password\")\n    private List<String> keystoreArgs = Arrays.asList(null, \"123456\", \"key0\", \"123456\");\n\n    @Parameter(names = {\"--manager\"}, description = \"Use manager (Cannot work with embedding modules)\")\n    private boolean useManager = false;\n\n    @Parameter(names = {\"-r\", \"--allowdown\"}, description = \"Allow downgrade installation by overriding versionCode to 1 (In most cases, the app can still get the correct versionCode)\")\n    private boolean overrideVersionCode = false;\n\n    @Parameter(names = {\"-v\", \"--verbose\"}, description = \"Verbose output\")\n    private boolean verbose = false;\n\n    @Parameter(names = {\"-m\", \"--embed\"}, description = \"Embed provided modules to apk\")\n    private List<String> modules = new ArrayList<>();\n\n    private static final String ANDROID_MANIFEST_XML = \"AndroidManifest.xml\";\n    private static final HashSet<String> ARCHES = new HashSet<>(Arrays.asList(\n            \"armeabi-v7a\",\n            \"arm64-v8a\",\n            \"x86\",\n            \"x86_64\"\n    ));\n\n    private static final ZFileOptions Z_FILE_OPTIONS = new ZFileOptions().setAlignmentRule(AlignmentRules.compose(\n            AlignmentRules.constantForSuffix(\".so\", 4096),\n            AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096)\n    ));\n\n    private final JCommander jCommander;\n\n    private final Logger logger;\n\n    public LSPatch(Logger logger, String... args) {\n        jCommander = JCommander.newBuilder().addObject(this).build();\n        try {\n            jCommander.parse(args);\n        } catch (ParameterException e) {\n            logger.e(e.getMessage() + \"\\n\");\n            help = true;\n        }\n        if (apkPaths == null || apkPaths.isEmpty()) {\n            logger.e(\"No apk specified\\n\");\n            help = true;\n        }\n        if (!modules.isEmpty() && useManager) {\n            logger.e(\"Should not use --embed and --manager at the same time\\n\");\n            help = true;\n        }\n\n        this.logger = logger;\n        logger.verbose = verbose;\n    }\n\n    public static void main(String... args) throws IOException {\n        LSPatch lspatch = new LSPatch(new JavaLogger(), args);\n        if (lspatch.help) {\n            lspatch.jCommander.usage();\n            return;\n        }\n        try {\n            lspatch.doCommandLine();\n        } catch (PatchError e) {\n            e.printStackTrace(System.err);\n        }\n    }\n\n    public void doCommandLine() throws PatchError, IOException {\n        for (var apk : apkPaths) {\n            File srcApkFile = new File(apk).getAbsoluteFile();\n\n            String apkFileName = srcApkFile.getName();\n\n            var outputDir = new File(outputPath);\n            outputDir.mkdirs();\n\n            File outputFile = new File(outputDir, String.format(\n                    Locale.getDefault(), \"%s-%d-lspatched.apk\",\n                    FilenameUtils.getBaseName(apkFileName),\n                    LSPConfig.instance.VERSION_CODE)\n            ).getAbsoluteFile();\n\n            if (outputFile.exists() && !forceOverwrite)\n                throw new PatchError(outputPath + \" exists. Use --force to overwrite\");\n            logger.i(\"Processing \" + srcApkFile + \" -> \" + outputFile);\n\n            patch(srcApkFile, outputFile);\n        }\n    }\n\n    public void patch(File srcApkFile, File outputFile) throws PatchError, IOException {\n        if (!srcApkFile.exists())\n            throw new PatchError(\"The source apk file does not exit. Please provide a correct path.\");\n\n        outputFile.delete();\n\n        logger.d(\"apk path: \" + srcApkFile);\n\n        logger.i(\"Parsing original apk...\");\n\n        try (var dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS);\n             var srcZFile = dstZFile.addNestedZip((ignore) -> ORIGINAL_APK_ASSET_PATH, srcApkFile, false)) {\n\n            // sign apk\n            try {\n                var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n                if (keystoreArgs.get(0) == null) {\n                    logger.i(\"Register apk signer with default keystore...\");\n                    try (var is = getClass().getClassLoader().getResourceAsStream(\"assets/keystore\")) {\n                        keyStore.load(is, keystoreArgs.get(1).toCharArray());\n                    }\n                } else {\n                    logger.i(\"Register apk signer with custom keystore...\");\n                    try (var is = new FileInputStream(keystoreArgs.get(0))) {\n                        keyStore.load(is, keystoreArgs.get(1).toCharArray());\n                    }\n                }\n                var entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(keystoreArgs.get(2), new KeyStore.PasswordProtection(keystoreArgs.get(3).toCharArray()));\n                new SigningExtension(SigningOptions.builder()\n                        .setMinSdkVersion(28)\n                        .setV2SigningEnabled(true)\n                        .setCertificates((X509Certificate[]) entry.getCertificateChain())\n                        .setKey(entry.getPrivateKey())\n                        .build()).register(dstZFile);\n            } catch (Exception e) {\n                throw new PatchError(\"Failed to register signer\", e);\n            }\n\n            String originalSignature = null;\n            if (sigbypassLevel > 0) {\n                originalSignature  = ApkSignatureHelper.getApkSignInfo(srcApkFile.getAbsolutePath());\n                if (originalSignature == null || originalSignature.isEmpty()) {\n                    throw new PatchError(\"get original signature failed\");\n                }\n                logger.d(\"Original signature\\n\" + originalSignature);\n            }\n\n            // copy out manifest file from zlib\n            var manifestEntry = srcZFile.get(ANDROID_MANIFEST_XML);\n            if (manifestEntry == null)\n                throw new PatchError(\"Provided file is not a valid apk\");\n\n            // parse the app appComponentFactory full name from the manifest file\n            final String appComponentFactory;\n            int minSdkVersion;\n            try (var is = manifestEntry.open()) {\n                var pair = ManifestParser.parseManifestFile(is);\n                if (pair == null)\n                    throw new PatchError(\"Failed to parse AndroidManifest.xml\");\n                appComponentFactory = pair.appComponentFactory;\n                minSdkVersion = pair.minSdkVersion;\n                logger.d(\"original appComponentFactory class: \" + appComponentFactory);\n                logger.d(\"original minSdkVersion: \" + minSdkVersion);\n            }\n\n            logger.i(\"Patching apk...\");\n            // modify manifest\n            final var config = new PatchConfig(useManager, debuggableFlag, overrideVersionCode, sigbypassLevel, originalSignature, appComponentFactory);\n            final var configBytes = new Gson().toJson(config).getBytes(StandardCharsets.UTF_8);\n            final var metadata = Base64.getEncoder().encodeToString(configBytes);\n            try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open(), metadata, minSdkVersion))) {\n                dstZFile.add(ANDROID_MANIFEST_XML, is);\n            } catch (Throwable e) {\n                throw new PatchError(\"Error when modifying manifest\", e);\n            }\n\n            logger.i(\"Adding config...\");\n            // save lspatch config to asset..\n            try (var is = new ByteArrayInputStream(configBytes)) {\n                dstZFile.add(CONFIG_ASSET_PATH, is);\n            } catch (Throwable e) {\n                throw new PatchError(\"Error when saving config\");\n            }\n\n            logger.i(\"Adding metaloader dex...\");\n            try (var is = getClass().getClassLoader().getResourceAsStream(Constants.META_LOADER_DEX_ASSET_PATH)) {\n                dstZFile.add(\"classes.dex\", is);\n            } catch (Throwable e) {\n                throw new PatchError(\"Error when adding dex\", e);\n            }\n\n            if (!useManager) {\n                logger.i(\"Adding loader dex...\");\n                try (var is = getClass().getClassLoader().getResourceAsStream(LOADER_DEX_ASSET_PATH)) {\n                    dstZFile.add(LOADER_DEX_ASSET_PATH, is);\n                } catch (Throwable e) {\n                    throw new PatchError(\"Error when adding assets\", e);\n                }\n\n                logger.i(\"Adding native lib...\");\n                // copy so and dex files into the unzipped apk\n                // do not put liblspatch.so into apk!lib because x86 native bridge causes crash\n                for (String arch : ARCHES) {\n                    String entryName = \"assets/lspatch/so/\" + arch + \"/liblspatch.so\";\n                    try (var is = getClass().getClassLoader().getResourceAsStream(entryName)) {\n                        dstZFile.add(entryName, is, false); // no compress for so\n                    } catch (Throwable e) {\n                        // More exception info\n                        throw new PatchError(\"Error when adding native lib\", e);\n                    }\n                    logger.d(\"added \" + entryName);\n                }\n\n                logger.i(\"Embedding modules...\");\n                embedModules(dstZFile);\n            }\n\n            // create zip link\n            logger.d(\"Creating nested apk link...\");\n\n            for (StoredEntry entry : srcZFile.entries()) {\n                String name = entry.getCentralDirectoryHeader().getName();\n                if (name.startsWith(\"classes\") && name.endsWith(\".dex\")) continue;\n                if (dstZFile.get(name) != null) continue;\n                if (name.equals(\"AndroidManifest.xml\")) continue;\n                if (name.startsWith(\"META-INF\") && (name.endsWith(\".SF\") || name.endsWith(\".MF\") || name.endsWith(\".RSA\"))) continue;\n                srcZFile.addFileLink(name, name);\n            }\n\n            dstZFile.realign();\n\n            logger.i(\"Writing apk...\");\n        }\n        logger.i(\"Done. Output APK: \" + outputFile.getAbsolutePath());\n    }\n\n    private void embedModules(ZFile zFile) {\n        for (var module : modules) {\n            File file = new File(module);\n            try (var apk = ZFile.openReadOnly(new File(module));\n                 var fileIs = new FileInputStream(file);\n                 var xmlIs = Objects.requireNonNull(apk.get(ANDROID_MANIFEST_XML)).open()\n            ) {\n                var manifest = Objects.requireNonNull(ManifestParser.parseManifestFile(xmlIs));\n                var packageName = manifest.packageName;\n                logger.i(\"  - \" + packageName);\n                zFile.add(EMBEDDED_MODULES_ASSET_PATH + packageName + \".apk\", fileIs);\n            } catch (NullPointerException | IOException e) {\n                logger.e(module + \" does not exist or is not a valid apk file.\");\n            }\n        }\n    }\n\n    private byte[] modifyManifestFile(InputStream is, String metadata, int minSdkVersion) throws IOException {\n        ModificationProperty property = new ModificationProperty();\n\n        if (overrideVersionCode)\n            property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, 1));\n        if (minSdkVersion < 28)\n            property.addUsesSdkAttribute(new AttributeItem(NodeValue.UsesSDK.MIN_SDK_VERSION, \"28\"));\n        property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggableFlag));\n        property.addApplicationAttribute(new AttributeItem(\"appComponentFactory\", PROXY_APP_COMPONENT_FACTORY));\n        property.addMetaData(new ModificationProperty.MetaData(\"lspatch\", metadata));\n        // TODO: replace query_all with queries -> manager\n        if (useManager)\n            property.addUsesPermission(\"android.permission.QUERY_ALL_PACKAGES\");\n\n        var os = new ByteArrayOutputStream();\n        (new ManifestEditor(is, os, property)).processManifest();\n        is.close();\n        os.flush();\n        os.close();\n        return os.toByteArray();\n    }\n}\n"
  },
  {
    "path": "patch/src/main/java/org/lsposed/patch/util/ApkSignatureHelper.java",
    "content": "package org.lsposed.patch.util;\n\nimport java.io.InputStream;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Arrays;\nimport java.security.cert.Certificate;\nimport java.util.Enumeration;\nimport java.util.jar.JarEntry;\nimport java.util.jar.JarFile;\n\n/**\n * Created by Wind\n */\npublic class ApkSignatureHelper {\n    private static final byte[] APK_V2_MAGIC = {'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ',\n            'B', 'l', 'o', 'c', 'k', ' ', '4', '2'};\n\n    private static char[] toChars(byte[] mSignature) {\n        byte[] sig = mSignature;\n        final int N = sig.length;\n        final int N2 = N * 2;\n        char[] text = new char[N2];\n        for (int j = 0; j < N; j++) {\n            byte v = sig[j];\n            int d = (v >> 4) & 0xf;\n            text[j * 2] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d));\n            d = v & 0xf;\n            text[j * 2 + 1] = (char) (d >= 10 ? ('a' + d - 10) : ('0' + d));\n        }\n        return text;\n    }\n\n    private static Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer) {\n        try {\n            InputStream is = jarFile.getInputStream(je);\n            while (is.read(readBuffer, 0, readBuffer.length) != -1) {\n            }\n            is.close();\n            return (Certificate[]) (je != null ? je.getCertificates() : null);\n        } catch (Exception e) {\n        }\n        return null;\n    }\n\n    public static String getApkSignInfo(String apkFilePath) {\n        try {\n            return getApkSignV2(apkFilePath);\n        } catch (Exception e) {\n            return getApkSignV1(apkFilePath);\n        }\n    }\n\n    public static String getApkSignV1(String apkFilePath) {\n        byte[] readBuffer = new byte[8192];\n        Certificate[] certs = null;\n        try {\n            JarFile jarFile = new JarFile(apkFilePath);\n            Enumeration<?> entries = jarFile.entries();\n            while (entries.hasMoreElements()) {\n                JarEntry je = (JarEntry) entries.nextElement();\n                if (je.isDirectory()) {\n                    continue;\n                }\n                if (je.getName().startsWith(\"META-INF/\")) {\n                    continue;\n                }\n                Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);\n                if (certs == null) {\n                    certs = localCerts;\n                } else {\n                    for (int i = 0; i < certs.length; i++) {\n                        boolean found = false;\n                        for (int j = 0; j < localCerts.length; j++) {\n                            if (certs[i] != null && certs[i].equals(localCerts[j])) {\n                                found = true;\n                                break;\n                            }\n                        }\n                        if (!found || certs.length != localCerts.length) {\n                            jarFile.close();\n                            return null;\n                        }\n                    }\n                }\n            }\n            jarFile.close();\n            return certs != null ? new String(toChars(certs[0].getEncoded())) : null;\n        } catch (Throwable ignored) {\n        }\n        return null;\n    }\n\n    private static String getApkSignV2(String apkFilePath) throws IOException {\n        try (RandomAccessFile apk = new RandomAccessFile(apkFilePath, \"r\")) {\n            ByteBuffer buffer = ByteBuffer.allocate(0x10);\n            buffer.order(ByteOrder.LITTLE_ENDIAN);\n\n            apk.seek(apk.length() - 0x6);\n            apk.readFully(buffer.array(), 0x0, 0x6);\n            int offset = buffer.getInt();\n            if (buffer.getShort() != 0) {\n                throw new UnsupportedEncodingException(\"no zip\");\n            }\n\n            apk.seek(offset - 0x10);\n            apk.readFully(buffer.array(), 0x0, 0x10);\n\n            if (!Arrays.equals(buffer.array(), APK_V2_MAGIC)) {\n                throw new UnsupportedEncodingException(\"no apk v2\");\n            }\n\n            // Read and compare size fields\n            apk.seek(offset - 0x18);\n            apk.readFully(buffer.array(), 0x0, 0x8);\n            buffer.rewind();\n            int size = (int) buffer.getLong();\n\n            ByteBuffer block = ByteBuffer.allocate(size + 0x8);\n            block.order(ByteOrder.LITTLE_ENDIAN);\n            apk.seek(offset - block.capacity());\n            apk.readFully(block.array(), 0x0, block.capacity());\n\n            if (size != block.getLong()) {\n                throw new UnsupportedEncodingException(\"no apk v2\");\n            }\n\n            while (block.remaining() > 24) {\n                size = (int) block.getLong();\n                if (block.getInt() == 0x7109871a) {\n                    // signer-sequence length, signer length, signed data length\n                    block.position(block.position() + 12);\n                    size = block.getInt(); // digests-sequence length\n\n                    // digests, certificates length\n                    block.position(block.position() + size + 0x4);\n\n                    size = block.getInt(); // certificate length\n                    break;\n                } else {\n                    block.position(block.position() + size - 0x4);\n                }\n            }\n\n            byte[] certificate = new byte[size];\n            block.get(certificate);\n\n            return new String(toChars(certificate));\n        }\n    }\n}\n"
  },
  {
    "path": "patch/src/main/java/org/lsposed/patch/util/JavaLogger.java",
    "content": "package org.lsposed.patch.util;\n\npublic class JavaLogger extends Logger {\n\n    @Override\n    public void d(String msg) {\n        if (verbose) System.out.println(msg);\n    }\n\n    @Override\n    public void i(String msg) {\n        System.out.println(msg);\n    }\n\n    @Override\n    public void e(String msg) {\n        System.err.println(msg);\n    }\n}\n"
  },
  {
    "path": "patch/src/main/java/org/lsposed/patch/util/Logger.java",
    "content": "package org.lsposed.patch.util;\n\npublic abstract class Logger {\n\n    public boolean verbose = false;\n\n    abstract public void d(String msg);\n\n    abstract public void i(String msg);\n\n    abstract public void e(String msg);\n}\n"
  },
  {
    "path": "patch/src/main/java/org/lsposed/patch/util/ManifestParser.java",
    "content": "package org.lsposed.patch.util;\n\nimport com.wind.meditor.utils.Utils;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\nimport pxb.android.axml.AxmlParser;\n\n/**\n * Created by Wind\n */\npublic class ManifestParser {\n\n    public static Pair parseManifestFile(InputStream is) throws IOException {\n        AxmlParser parser = new AxmlParser(Utils.getBytesFromInputStream(is));\n        String packageName = null;\n        String appComponentFactory = null;\n        int minSdkVersion = 0;\n        try {\n\n            while (true) {\n                int type = parser.next();\n                if (type == AxmlParser.END_FILE) {\n                    break;\n                }\n                if (type == AxmlParser.START_TAG) {\n                    int attrCount = parser.getAttributeCount();\n                    for (int i = 0; i < attrCount; i++) {\n                        String attrName = parser.getAttrName(i);\n                        int attrNameRes = parser.getAttrResId(i);\n\n                        String name = parser.getName();\n                        \n                        if (\"manifest\".equals(name)) {\n                            if (\"package\".equals(attrName)) {\n                                packageName = parser.getAttrValue(i).toString();\n                            }\n                        }\n\n                        if (\"uses-sdk\".equals(name)) {\n                            if (\"minSdkVersion\".equals(attrName)) {\n                                minSdkVersion = Integer.parseInt(parser.getAttrValue(i).toString());\n                            }\n                        }\n\n                        if (\"appComponentFactory\".equals(attrName) || attrNameRes == 0x0101057a) {\n                            appComponentFactory = parser.getAttrValue(i).toString();\n                        }\n\n                        if (packageName != null && packageName.length() > 0 &&\n                                appComponentFactory != null && appComponentFactory.length() > 0 &&\n                                minSdkVersion > 0\n                        ) {\n                            return new Pair(packageName, appComponentFactory, minSdkVersion);\n                        }\n                    }\n                } else if (type == AxmlParser.END_TAG) {\n                    // ignored\n                }\n            }\n        } catch (Exception e) {\n            return null;\n        }\n\n        return new Pair(packageName, appComponentFactory, minSdkVersion);\n    }\n\n    /**\n     * Get the package name and the main application name from the manifest file\n     */\n    public static Pair parseManifestFile(String filePath) throws IOException {\n        File file = new File(filePath);\n        try (var is = new FileInputStream(file)) {\n            return parseManifestFile(is);\n        }\n    }\n\n    public static class Pair {\n        public String packageName;\n        public String appComponentFactory;\n\n        public int minSdkVersion;\n\n        public Pair(String packageName, String appComponentFactory, int minSdkVersion) {\n            this.packageName = packageName;\n            this.appComponentFactory = appComponentFactory;\n            this.minSdkVersion = minSdkVersion;\n        }\n    }\n\n}\n"
  },
  {
    "path": "patch-loader/.gitignore",
    "content": "/build\n/.cxx\n"
  },
  {
    "path": "patch-loader/build.gradle.kts",
    "content": "import java.util.Locale\n\nplugins {\n    alias(libs.plugins.agp.app)\n}\n\nandroid {\n    defaultConfig {\n        multiDexEnabled = false\n    }\n\n    buildFeatures {\n        buildConfig = true\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\")\n        }\n    }\n\n    externalNativeBuild {\n        cmake {\n            path(\"src/main/jni/CMakeLists.txt\")\n        }\n    }\n    namespace = \"org.lsposed.lspatch.loader\"\n}\n\nandroidComponents.onVariants { variant ->\n    val variantCapped = variant.name.replaceFirstChar { it.uppercase() }\n\n    task<Copy>(\"copyDex$variantCapped\") {\n        dependsOn(\"assemble$variantCapped\")\n        from(\"$buildDir/intermediates/dex/${variant.name}/mergeDex$variantCapped/classes.dex\")\n        rename(\"classes.dex\", \"loader.dex\")\n        into(\"${rootProject.projectDir}/out/assets/${variant.name}/lspatch\")\n    }\n\n    task<Copy>(\"copySo$variantCapped\") {\n        dependsOn(\"assemble$variantCapped\")\n        from(\n            fileTree(\n                \"dir\" to \"$buildDir/intermediates/stripped_native_libs/${variant.name}/out/lib\",\n                \"include\" to listOf(\"**/liblspatch.so\")\n            )\n        )\n        into(\"${rootProject.projectDir}/out/assets/${variant.name}/lspatch/so\")\n    }\n\n    task(\"copy$variantCapped\") {\n        dependsOn(\"copySo$variantCapped\")\n        dependsOn(\"copyDex$variantCapped\")\n\n        doLast {\n            println(\"Dex and so files has been copied to ${rootProject.projectDir}${File.separator}out\")\n        }\n    }\n}\n\ndependencies {\n    compileOnly(projects.hiddenapi.stubs)\n    implementation(projects.core)\n    implementation(projects.hiddenapi.bridge)\n    implementation(projects.services.daemonService)\n    implementation(projects.share.android)\n    implementation(projects.share.java)\n\n    implementation(libs.gson)\n}\n"
  },
  {
    "path": "patch-loader/proguard-rules.pro",
    "content": "-keep class com.wind.xposed.entry.MMPEntry {\n    public <init>();\n    public void initAndLoadModules();\n}\n\n-keep class com.wind.xpatch.proxy.**{*;}\n\n-keep class de.robv.android.xposed.**{*;}\n\n-keep class android.app.**{*;}\n-keep class android.content.**{*;}\n-keep class android.os.**{*;}\n\n-keep class android.view.**{*;}\n-keep class com.lody.whale.**{*;}\n-keep class com.android.internal.**{*;}\n-keep class xposed.dummy.**{*;}\n-keep class com.wind.xposed.entry.util.**{*;}\n\n-keep class com.swift.sandhook.**{*;}\n-keep class com.swift.sandhook.xposedcompat.**{*;}\n\n-dontwarn android.content.res.Resources\n-dontwarn android.content.res.Resources$Theme\n-dontwarn android.content.res.AssetManager\n-dontwarn android.content.res.TypedArray\n"
  },
  {
    "path": "patch-loader/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPApplication.java",
    "content": "package org.lsposed.lspatch.loader;\n\nimport static org.lsposed.lspatch.share.Constants.CONFIG_ASSET_PATH;\nimport static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH;\n\nimport android.app.ActivityThread;\nimport android.app.LoadedApk;\nimport android.content.Context;\nimport android.content.pm.ApplicationInfo;\nimport android.content.res.CompatibilityInfo;\nimport android.os.Build;\nimport android.os.RemoteException;\nimport android.system.Os;\nimport android.util.Log;\n\nimport com.google.gson.Gson;\n\nimport org.lsposed.lspatch.loader.util.FileUtils;\nimport org.lsposed.lspatch.loader.util.XLog;\nimport org.lsposed.lspatch.service.LocalApplicationService;\nimport org.lsposed.lspatch.service.RemoteApplicationService;\nimport org.lsposed.lspatch.share.PatchConfig;\nimport org.lsposed.lspd.core.Startup;\nimport org.lsposed.lspd.service.ILSPApplicationService;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.lang.reflect.Field;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.PosixFilePermissions;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.zip.ZipFile;\n\nimport de.robv.android.xposed.XposedHelpers;\nimport hidden.HiddenApiBridge;\n\n/**\n * Created by Windysha\n */\n@SuppressWarnings(\"unused\")\npublic class LSPApplication {\n\n    private static final String TAG = \"LSPatch\";\n    private static final int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000;\n    private static final int PER_USER_RANGE = 100000;\n\n    private static ActivityThread activityThread;\n    private static LoadedApk stubLoadedApk;\n    private static LoadedApk appLoadedApk;\n\n    private static PatchConfig config;\n\n    public static boolean isIsolated() {\n        return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID;\n    }\n\n    public static void onLoad() throws RemoteException, IOException {\n        if (isIsolated()) {\n            XLog.d(TAG, \"Skip isolated process\");\n            return;\n        }\n        activityThread = ActivityThread.currentActivityThread();\n        var context = createLoadedApkWithContext();\n        if (context == null) {\n            XLog.e(TAG, \"Error when creating context\");\n            return;\n        }\n\n        Log.d(TAG, \"Initialize service client\");\n        ILSPApplicationService service;\n        if (config.useManager) {\n            service = new RemoteApplicationService(context);\n        } else {\n            service = new LocalApplicationService(context);\n        }\n\n        disableProfile(context);\n        Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service);\n        Startup.bootstrapXposed();\n        // WARN: Since it uses `XResource`, the following class should not be initialized\n        // before forkPostCommon is invoke. Otherwise, you will get failure of XResources\n        Log.i(TAG, \"Load modules\");\n        LSPLoader.initModules(appLoadedApk);\n        Log.i(TAG, \"Modules initialized\");\n\n        switchAllClassLoader();\n        SigBypass.doSigBypass(context, config.sigBypassLevel);\n\n        Log.i(TAG, \"LSPatch bootstrap completed\");\n    }\n\n    private static Context createLoadedApkWithContext() {\n        try {\n            var mBoundApplication = XposedHelpers.getObjectField(activityThread, \"mBoundApplication\");\n\n            stubLoadedApk = (LoadedApk) XposedHelpers.getObjectField(mBoundApplication, \"info\");\n            var appInfo = (ApplicationInfo) XposedHelpers.getObjectField(mBoundApplication, \"appInfo\");\n            var compatInfo = (CompatibilityInfo) XposedHelpers.getObjectField(mBoundApplication, \"compatInfo\");\n            var baseClassLoader = stubLoadedApk.getClassLoader();\n\n            try (var is = baseClassLoader.getResourceAsStream(CONFIG_ASSET_PATH)) {\n                BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));\n                config = new Gson().fromJson(streamReader, PatchConfig.class);\n            } catch (IOException e) {\n                Log.e(TAG, \"Failed to load config file\");\n                return null;\n            }\n            Log.i(TAG, \"Use manager: \" + config.useManager);\n            Log.i(TAG, \"Signature bypass level: \" + config.sigBypassLevel);\n\n            Path originPath = Paths.get(appInfo.dataDir, \"cache/lspatch/origin/\");\n            Path cacheApkPath;\n            try (ZipFile sourceFile = new ZipFile(appInfo.sourceDir)) {\n                cacheApkPath = originPath.resolve(sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + \".apk\");\n            }\n\n            appInfo.sourceDir = cacheApkPath.toString();\n            appInfo.publicSourceDir = cacheApkPath.toString();\n            appInfo.appComponentFactory = config.appComponentFactory;\n\n            if (!Files.exists(cacheApkPath)) {\n                Log.i(TAG, \"Extract original apk\");\n                FileUtils.deleteFolderIfExists(originPath);\n                Files.createDirectories(originPath);\n                try (InputStream is = baseClassLoader.getResourceAsStream(ORIGINAL_APK_ASSET_PATH)) {\n                    Files.copy(is, cacheApkPath);\n                }\n            }\n            cacheApkPath.toFile().setWritable(false);\n\n            var mPackages = (Map<?, ?>) XposedHelpers.getObjectField(activityThread, \"mPackages\");\n            mPackages.remove(appInfo.packageName);\n            appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);\n            XposedHelpers.setObjectField(mBoundApplication, \"info\", appLoadedApk);\n\n            var activityClientRecordClass = XposedHelpers.findClass(\"android.app.ActivityThread$ActivityClientRecord\", ActivityThread.class.getClassLoader());\n            var fixActivityClientRecord = (BiConsumer<Object, Object>) (k, v) -> {\n                if (activityClientRecordClass.isInstance(v)) {\n                    var pkgInfo = XposedHelpers.getObjectField(v, \"packageInfo\");\n                    if (pkgInfo == stubLoadedApk) {\n                        Log.d(TAG, \"fix loadedapk from ActivityClientRecord\");\n                        XposedHelpers.setObjectField(v, \"packageInfo\", appLoadedApk);\n                    }\n                }\n            };\n            var mActivities = (Map<?, ?>) XposedHelpers.getObjectField(activityThread, \"mActivities\");\n            mActivities.forEach(fixActivityClientRecord);\n            try {\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n                    var mLaunchingActivities = (Map<?, ?>) XposedHelpers.getObjectField(activityThread, \"mLaunchingActivities\");\n                    mLaunchingActivities.forEach(fixActivityClientRecord);\n                }\n            } catch (Throwable ignored) {\n            }\n            Log.i(TAG, \"hooked app initialized: \" + appLoadedApk);\n\n            var context = (Context) XposedHelpers.callStaticMethod(Class.forName(\"android.app.ContextImpl\"), \"createAppContext\", activityThread, stubLoadedApk);\n            if (config.appComponentFactory != null) {\n                try {\n                    context.getClassLoader().loadClass(config.appComponentFactory);\n                } catch (ClassNotFoundException e) { // This will happen on some strange shells like 360\n                    Log.w(TAG, \"Original AppComponentFactory not found: \" + config.appComponentFactory);\n                    appInfo.appComponentFactory = null;\n                }\n            }\n            return context;\n        } catch (Throwable e) {\n            Log.e(TAG, \"createLoadedApk\", e);\n            return null;\n        }\n    }\n\n    public static void disableProfile(Context context) {\n        final ArrayList<String> codePaths = new ArrayList<>();\n        var appInfo = context.getApplicationInfo();\n        var pkgName = context.getPackageName();\n        if (appInfo == null) return;\n        if ((appInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) {\n            codePaths.add(appInfo.sourceDir);\n        }\n        if (appInfo.splitSourceDirs != null) {\n            Collections.addAll(codePaths, appInfo.splitSourceDirs);\n        }\n\n        if (codePaths.isEmpty()) {\n            // If there are no code paths there's no need to setup a profile file and register with\n            // the runtime,\n            return;\n        }\n\n        var profileDir = HiddenApiBridge.Environment_getDataProfilesDePackageDirectory(appInfo.uid / PER_USER_RANGE, pkgName);\n\n        var attrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString(\"r--------\"));\n\n        for (int i = codePaths.size() - 1; i >= 0; i--) {\n            String splitName = i == 0 ? null : appInfo.splitNames[i - 1];\n            File curProfileFile = new File(profileDir, splitName == null ? \"primary.prof\" : splitName + \".split.prof\").getAbsoluteFile();\n            Log.d(TAG, \"Processing \" + curProfileFile.getAbsolutePath());\n            try {\n                if (!curProfileFile.canWrite() && Files.size(curProfileFile.toPath()) == 0) {\n                    Log.d(TAG, \"Skip profile \" + curProfileFile.getAbsolutePath());\n                    continue;\n                }\n                if (curProfileFile.exists() && !curProfileFile.delete()) {\n                    try (var writer = new FileOutputStream(curProfileFile)) {\n                        Log.d(TAG, \"Failed to delete, try to clear content \" + curProfileFile.getAbsolutePath());\n                    } catch (Throwable e) {\n                        Log.e(TAG, \"Failed to delete and clear profile file \" + curProfileFile.getAbsolutePath(), e);\n                    }\n                    Os.chmod(curProfileFile.getAbsolutePath(), 00400);\n                } else {\n                    Files.createFile(curProfileFile.toPath(), attrs);\n                }\n            } catch (Throwable e) {\n                Log.e(TAG, \"Failed to disable profile file \" + curProfileFile.getAbsolutePath(), e);\n            }\n        }\n    }\n\n    private static void switchAllClassLoader() {\n        var fields = LoadedApk.class.getDeclaredFields();\n        for (Field field : fields) {\n            if (field.getType() == ClassLoader.class) {\n                var obj = XposedHelpers.getObjectField(appLoadedApk, field.getName());\n                XposedHelpers.setObjectField(stubLoadedApk, field.getName(), obj);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/loader/LSPLoader.java",
    "content": "package org.lsposed.lspatch.loader;\n\nimport android.app.ActivityThread;\nimport android.app.LoadedApk;\nimport android.content.res.XResources;\n\nimport de.robv.android.xposed.XposedBridge;\nimport de.robv.android.xposed.XposedInit;\nimport de.robv.android.xposed.callbacks.XC_LoadPackage;\n\npublic class LSPLoader {\n    public static void initModules(LoadedApk loadedApk) {\n        XposedInit.loadedPackagesInProcess.add(loadedApk.getPackageName());\n        XResources.setPackageNameForResDir(loadedApk.getPackageName(), loadedApk.getResDir());\n        XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(\n                XposedBridge.sLoadedPackageCallbacks);\n        lpparam.packageName = loadedApk.getPackageName();\n        lpparam.processName = ActivityThread.currentProcessName();\n        lpparam.classLoader = loadedApk.getClassLoader();\n        lpparam.appInfo = loadedApk.getApplicationInfo();\n        lpparam.isFirstApplication = true;\n        XC_LoadPackage.callAll(lpparam);\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/loader/SigBypass.java",
    "content": "package org.lsposed.lspatch.loader;\n\nimport static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH;\n\nimport android.content.Context;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.content.pm.PackageParser;\nimport android.content.pm.Signature;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.util.Base64;\nimport android.util.Log;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonSyntaxException;\n\nimport org.lsposed.lspatch.loader.util.XLog;\nimport org.lsposed.lspatch.share.Constants;\nimport org.lsposed.lspatch.share.PatchConfig;\n\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.zip.ZipFile;\n\nimport de.robv.android.xposed.XC_MethodHook;\nimport de.robv.android.xposed.XposedBridge;\nimport de.robv.android.xposed.XposedHelpers;\n\npublic class SigBypass {\n\n    private static final String TAG = \"LSPatch-SigBypass\";\n    private static final Map<String, String> signatures = new HashMap<>();\n\n    private static void replaceSignature(Context context, PackageInfo packageInfo) {\n        boolean hasSignature = (packageInfo.signatures != null && packageInfo.signatures.length != 0) || packageInfo.signingInfo != null;\n        if (hasSignature) {\n            String packageName = packageInfo.packageName;\n            String replacement = signatures.get(packageName);\n            if (replacement == null && !signatures.containsKey(packageName)) {\n                try {\n                    var metaData = context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData;\n                    String encoded = null;\n                    if (metaData != null) encoded = metaData.getString(\"lspatch\");\n                    if (encoded != null) {\n                        var json = new String(Base64.decode(encoded, Base64.DEFAULT), StandardCharsets.UTF_8);\n                        var patchConfig = new Gson().fromJson(json, PatchConfig.class);\n                        replacement = patchConfig.originalSignature;\n                    }\n                } catch (PackageManager.NameNotFoundException | JsonSyntaxException ignored) {\n                }\n                signatures.put(packageName, replacement);\n            }\n            if (replacement != null) {\n                if (packageInfo.signatures != null && packageInfo.signatures.length > 0) {\n                    XLog.d(TAG, \"Replace signature info for `\" + packageName + \"` (method 1)\");\n                    packageInfo.signatures[0] = new Signature(replacement);\n                }\n                if (packageInfo.signingInfo != null) {\n                    XLog.d(TAG, \"Replace signature info for `\" + packageName + \"` (method 2)\");\n                    Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners();\n                    if (signaturesArray != null && signaturesArray.length > 0) {\n                        signaturesArray[0] = new Signature(replacement);\n                    }\n                }\n            }\n        }\n    }\n\n    private static void hookPackageParser(Context context) {\n        XposedBridge.hookAllMethods(PackageParser.class, \"generatePackageInfo\", new XC_MethodHook() {\n            @Override\n            protected void afterHookedMethod(MethodHookParam param) {\n                var packageInfo = (PackageInfo) param.getResult();\n                if (packageInfo == null) return;\n                replaceSignature(context, packageInfo);\n            }\n        });\n    }\n\n    private static void proxyPackageInfoCreator(Context context) {\n        Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR;\n        Parcelable.Creator<PackageInfo> proxiedCreator = new Parcelable.Creator<>() {\n            @Override\n            public PackageInfo createFromParcel(Parcel source) {\n                PackageInfo packageInfo = originalCreator.createFromParcel(source);\n                replaceSignature(context, packageInfo);\n                return packageInfo;\n            }\n\n            @Override\n            public PackageInfo[] newArray(int size) {\n                return originalCreator.newArray(size);\n            }\n        };\n        XposedHelpers.setStaticObjectField(PackageInfo.class, \"CREATOR\", proxiedCreator);\n        try {\n            Map<?, ?> mCreators = (Map<?, ?>) XposedHelpers.getStaticObjectField(Parcel.class, \"mCreators\");\n            mCreators.clear();\n        } catch (NoSuchFieldError ignore) {\n        } catch (Throwable e) {\n            Log.w(TAG, \"fail to clear Parcel.mCreators\", e);\n        }\n        try {\n            Map<?, ?> sPairedCreators = (Map<?, ?>) XposedHelpers.getStaticObjectField(Parcel.class, \"sPairedCreators\");\n            sPairedCreators.clear();\n        } catch (NoSuchFieldError ignore) {\n        } catch (Throwable e) {\n            Log.w(TAG, \"fail to clear Parcel.sPairedCreators\", e);\n        }\n    }\n\n    static void doSigBypass(Context context, int sigBypassLevel) throws IOException {\n        if (sigBypassLevel >= Constants.SIGBYPASS_LV_PM) {\n            hookPackageParser(context);\n            proxyPackageInfoCreator(context);\n        }\n        if (sigBypassLevel >= Constants.SIGBYPASS_LV_PM_OPENAT) {\n            String cacheApkPath;\n            try (ZipFile sourceFile = new ZipFile(context.getPackageResourcePath())) {\n                cacheApkPath = context.getCacheDir() + \"/lspatch/origin/\" + sourceFile.getEntry(ORIGINAL_APK_ASSET_PATH).getCrc() + \".apk\";\n            }\n            org.lsposed.lspd.nativebridge.SigBypass.enableOpenatHook(context.getPackageResourcePath(), cacheApkPath);\n        }\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/loader/util/FileUtils.java",
    "content": "package org.lsposed.lspatch.loader.util;\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\npublic class FileUtils {\n\n    public static void deleteFolderIfExists(Path target) throws IOException {\n        if (Files.notExists(target)) return;\n        Files.walkFileTree(target, new SimpleFileVisitor<>() {\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)\n                    throws IOException {\n                Files.delete(file);\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult postVisitDirectory(Path dir, IOException e)\n                    throws IOException {\n                if (e == null) {\n                    Files.delete(dir);\n                    return FileVisitResult.CONTINUE;\n                } else {\n                    throw e;\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/loader/util/XLog.java",
    "content": "package org.lsposed.lspatch.loader.util;\n\nimport org.lsposed.lspatch.loader.BuildConfig;\n\npublic class XLog {\n\n    private static boolean enableLog = BuildConfig.DEBUG;\n\n    public static void d(String tag, String msg) {\n        if (enableLog) {\n            android.util.Log.d(tag, msg);\n        }\n    }\n\n    public static void v(String tag, String msg) {\n        if (enableLog) {\n            android.util.Log.v(tag, msg);\n        }\n    }\n\n    public static void w(String tag, String msg) {\n        if (enableLog) {\n            android.util.Log.w(tag, msg);\n        }\n    }\n\n    public static void i(String tag, String msg) {\n        if (enableLog) {\n            android.util.Log.i(tag, msg);\n        }\n    }\n\n    public static void e(String tag, String msg) {\n        if (enableLog) {\n            android.util.Log.e(tag, msg);\n        }\n    }\n\n    public static void e(String tag, String msg, Throwable tr) {\n        if (enableLog) {\n            android.util.Log.e(tag, msg, tr);\n        }\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/service/LocalApplicationService.java",
    "content": "package org.lsposed.lspatch.service;\n\nimport android.content.Context;\nimport android.os.Environment;\nimport android.os.IBinder;\nimport android.os.ParcelFileDescriptor;\nimport android.os.RemoteException;\nimport android.util.Log;\n\nimport org.lsposed.lspatch.loader.util.FileUtils;\nimport org.lsposed.lspatch.share.Constants;\nimport org.lsposed.lspatch.util.ModuleLoader;\nimport org.lsposed.lspd.models.Module;\nimport org.lsposed.lspd.service.ILSPApplicationService;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.zip.ZipFile;\n\npublic class LocalApplicationService extends ILSPApplicationService.Stub {\n\n    private static final String TAG = \"LSPatch\";\n\n    private final List<Module> modules = new ArrayList<>();\n\n    public LocalApplicationService(Context context) {\n        try {\n            for (var name : context.getAssets().list(\"lspatch/modules\")) {\n                String packageName = name.substring(0, name.length() - 4);\n                String modulePath = context.getCacheDir() + \"/lspatch/\" + packageName + \"/\";\n                String cacheApkPath;\n                try (ZipFile sourceFile = new ZipFile(context.getPackageResourcePath())) {\n                    cacheApkPath = modulePath + sourceFile.getEntry(Constants.EMBEDDED_MODULES_ASSET_PATH + name).getCrc() + \".apk\";\n                }\n\n                if (!Files.exists(Paths.get(cacheApkPath))) {\n                    Log.i(TAG, \"Extract module apk: \" + packageName);\n                    FileUtils.deleteFolderIfExists(Paths.get(modulePath));\n                    Files.createDirectories(Paths.get(modulePath));\n                    try (var is = context.getAssets().open(\"lspatch/modules/\" + name)) {\n                        Files.copy(is, Paths.get(cacheApkPath));\n                    }\n                }\n\n                var module = new Module();\n                module.apkPath = cacheApkPath;\n                module.packageName = packageName;\n                module.file = ModuleLoader.loadModule(cacheApkPath);\n                modules.add(module);\n            }\n        } catch (IOException e) {\n            Log.e(TAG, \"Error when initializing LocalApplicationServiceClient\", e);\n        }\n    }\n\n    @Override\n    public List<Module> getLegacyModulesList() {\n        return modules;\n    }\n\n    @Override\n    public List<Module> getModulesList() {\n        return new ArrayList<>();\n    }\n\n    @Override\n    public String getPrefsPath(String packageName) {\n        return new File(Environment.getDataDirectory(), \"data/\" + packageName + \"/shared_prefs/\").getAbsolutePath();\n    }\n\n    @Override\n    public ParcelFileDescriptor requestInjectedManagerBinder(List<IBinder> binder) {\n        return null;\n    }\n\n    @Override\n    public IBinder asBinder() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspatch/service/RemoteApplicationService.java",
    "content": "package org.lsposed.lspatch.service;\n\nimport android.annotation.SuppressLint;\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.ServiceConnection;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.IBinder;\nimport android.os.ParcelFileDescriptor;\nimport android.os.RemoteException;\nimport android.os.UserHandle;\nimport android.util.Log;\nimport android.widget.Toast;\n\nimport org.lsposed.lspatch.share.Constants;\nimport org.lsposed.lspd.models.Module;\nimport org.lsposed.lspd.service.ILSPApplicationService;\n\nimport java.io.File;\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\npublic class RemoteApplicationService implements ILSPApplicationService {\n\n    private static final String TAG = \"LSPatch\";\n    private static final String MODULE_SERVICE = Constants.MANAGER_PACKAGE_NAME + \".manager.ModuleService\";\n\n    private volatile ILSPApplicationService service;\n\n    @SuppressLint(\"DiscouragedPrivateApi\")\n    public RemoteApplicationService(Context context) throws RemoteException {\n        try {\n            var intent = new Intent()\n                    .setComponent(new ComponentName(Constants.MANAGER_PACKAGE_NAME, MODULE_SERVICE))\n                    .putExtra(\"packageName\", context.getPackageName());\n            // TODO: Authentication\n            var latch = new CountDownLatch(1);\n            var conn = new ServiceConnection() {\n                @Override\n                public void onServiceConnected(ComponentName name, IBinder binder) {\n                    Log.i(TAG, \"Manager binder received\");\n                    service = Stub.asInterface(binder);\n                    latch.countDown();\n                }\n\n                @Override\n                public void onServiceDisconnected(ComponentName name) {\n                    Log.e(TAG, \"Manager service died\");\n                    service = null;\n                }\n            };\n            Log.i(TAG, \"Request manager binder\");\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                context.bindService(intent, Context.BIND_AUTO_CREATE, Executors.newSingleThreadExecutor(), conn);\n            } else {\n                var handlerThread = new HandlerThread(\"RemoteApplicationService\");\n                handlerThread.start();\n                var handler = new Handler(handlerThread.getLooper());\n                var contextImplClass = context.getClass();\n                var getUserMethod = contextImplClass.getMethod(\"getUser\");\n                var bindServiceAsUserMethod = contextImplClass.getDeclaredMethod(\n                        \"bindServiceAsUser\", Intent.class, ServiceConnection.class, int.class, Handler.class, UserHandle.class);\n                var userHandle = (UserHandle) getUserMethod.invoke(context);\n                bindServiceAsUserMethod.invoke(context, intent, conn, Context.BIND_AUTO_CREATE, handler, userHandle);\n            }\n            boolean success = latch.await(1, TimeUnit.SECONDS);\n            if (!success) throw new TimeoutException(\"Bind service timeout\");\n        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |\n                 InterruptedException | TimeoutException e) {\n            Toast.makeText(context, \"Unable to connect to Manager\", Toast.LENGTH_SHORT).show();\n            var r = new RemoteException(\"Failed to get manager binder\");\n            r.initCause(e);\n            throw r;\n        }\n    }\n\n    @Override\n    public List<Module> getLegacyModulesList() throws RemoteException {\n        return service == null ? new ArrayList<>() : service.getLegacyModulesList();\n    }\n\n    @Override\n    public List<Module> getModulesList() throws RemoteException {\n        return service == null ? new ArrayList<>() : service.getModulesList();\n    }\n\n    @Override\n    public String getPrefsPath(String packageName) {\n        return new File(Environment.getDataDirectory(), \"data/\" + packageName + \"/shared_prefs/\").getAbsolutePath();\n    }\n\n    @Override\n    public IBinder asBinder() {\n        return service == null ? null : service.asBinder();\n    }\n\n    @Override\n    public ParcelFileDescriptor requestInjectedManagerBinder(List<IBinder> binder) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/java/org/lsposed/lspd/nativebridge/SigBypass.java",
    "content": "package org.lsposed.lspd.nativebridge;\n\npublic class SigBypass {\n    public static native void enableOpenatHook(String origApkPath, String cacheApkPath);\n}\n"
  },
  {
    "path": "patch-loader/src/main/jni/CMakeLists.txt",
    "content": "project(lspatch)\ncmake_minimum_required(VERSION 3.4.1)\n\nadd_subdirectory(${CORE_ROOT} core)\n\naux_source_directory(src SRC_LIST)\naux_source_directory(src/jni SRC_LIST)\nset(SRC_LIST ${SRC_LIST} api/patch_main.cpp)\n\nadd_library(${PROJECT_NAME} SHARED ${SRC_LIST})\n\ntarget_include_directories(${PROJECT_NAME} PUBLIC include)\ntarget_include_directories(${PROJECT_NAME} PRIVATE src)\n\ntarget_link_libraries(${PROJECT_NAME} core log)\n\nif (DEFINED DEBUG_SYMBOLS_PATH)\n    set(DEBUG_SYMBOLS_PATH ${DEBUG_SYMBOLS_PATH}/${API})\n    message(STATUS \"Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}\")\n    add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD\n            COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}\n            COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $<TARGET_FILE:${PROJECT_NAME}>\n            ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug\n            COMMAND ${CMAKE_STRIP} --strip-all $<TARGET_FILE:${PROJECT_NAME}>\n            COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug\n            $<TARGET_FILE:${PROJECT_NAME}>)\nendif()\n"
  },
  {
    "path": "patch-loader/src/main/jni/api/patch_main.cpp",
    "content": "/*\n * This file is part of LSPosed.\n *\n * LSPosed is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * LSPosed is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n *\n * Copyright (C) 2022 LSPosed Contributors\n */\n\n//\n// Created by Nullptr on 2022/3/17.\n//\n\n#include <jni.h>\n\n#include \"config_impl.h\"\n#include \"patch_loader.h\"\n\nJNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {\n    JNIEnv* env;\n    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {\n        return JNI_ERR;\n    }\n    lspd::PatchLoader::Init();\n    lspd::ConfigImpl::Init();\n    lspd::PatchLoader::GetInstance()->Load(env);\n    return JNI_VERSION_1_6;\n}\n"
  },
  {
    "path": "patch-loader/src/main/jni/include/art/runtime/jit/profile_saver.h",
    "content": "//\n// Created by loves on 6/19/2021.\n//\n\n#ifndef LSPATCH_PROFILE_SAVER_H\n#define LSPATCH_PROFILE_SAVER_H\n\n#include \"utils/hook_helper.hpp\"\n\nusing namespace lsplant;\n\nnamespace art {\n    CREATE_MEM_HOOK_STUB_ENTRY(\n            \"_ZN3art12ProfileSaver20ProcessProfilingInfoEbPt\",\n            bool, ProcessProfilingInfo, (void * thiz, bool, uint16_t *), {\n                LOGD(\"skipped profile saving\");\n                return true;\n            });\n\n    CREATE_MEM_HOOK_STUB_ENTRY(\n            \"_ZN3art12ProfileSaver20ProcessProfilingInfoEbbPt\",\n            bool, ProcessProfilingInfoWithBool, (void * thiz, bool, bool, uint16_t *), {\n                LOGD(\"skipped profile saving\");\n                return true;\n            });\n\n    CREATE_HOOK_STUB_ENTRY(\n            \"execve\",\n            int, execve, (const char *pathname, const char *argv[], char *const envp[]), {\n                if (strstr(pathname, \"dex2oat\")) {\n                    size_t count = 0;\n                    while (argv[count++] != nullptr);\n                    std::unique_ptr<const char *[]> new_args = std::make_unique<const char *[]>(\n                            count + 1);\n                    for (size_t i = 0; i < count - 1; ++i)\n                        new_args[i] = argv[i];\n                    new_args[count - 1] = \"--inline-max-code-units=0\";\n                    new_args[count] = nullptr;\n\n                    LOGD(\"dex2oat by disable inline!\");\n                    int ret = backup(pathname, new_args.get(), envp);\n                    return ret;\n                }\n                int ret = backup(pathname, argv, envp);\n                return ret;\n            });\n\n\n    static void DisableInline(const HookHandler &handler) {\n        HookSyms(handler, ProcessProfilingInfo, ProcessProfilingInfoWithBool);\n        HookSymNoHandle(handler, reinterpret_cast<void*>(&::execve), execve);\n    }\n}\n\n\n#endif //LSPATCH_PROFILE_SAVER_H\n"
  },
  {
    "path": "patch-loader/src/main/jni/include/art/runtime/oat_file_manager.h",
    "content": "/*\n * This file is part of LSPosed.\n *\n * LSPosed is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * LSPosed is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n *\n * Copyright (C) 2021 - 2022 LSPosed Contributors\n */\n\n#ifndef LSPATCH_OAT_FILE_MANAGER_H\n#define LSPATCH_OAT_FILE_MANAGER_H\n\n#include \"context.h\"\n#include \"utils/hook_helper.hpp\"\n\nusing namespace lsplant;\n\nnamespace art {\n    CREATE_MEM_HOOK_STUB_ENTRY(\n            \"_ZN3art14OatFileManager25RunBackgroundVerificationERKNSt3__16vectorIPKNS_7DexFileENS1_9allocatorIS5_EEEEP8_jobjectPKc\",\n            void, RunBackgroundVerificationWithContext,\n            (void * thiz, const std::vector<const void *> &dex_files,\n                    jobject class_loader,\n                    const char *class_loader_context), {\n                if (lspd::Context::GetInstance()->GetCurrentClassLoader() == nullptr) {\n                    LOGD(\"Disabled background verification\");\n                    return;\n                }\n                backup(thiz, dex_files, class_loader, class_loader_context);\n            });\n\n    CREATE_MEM_HOOK_STUB_ENTRY(\n            \"_ZN3art14OatFileManager25RunBackgroundVerificationERKNSt3__16vectorIPKNS_7DexFileENS1_9allocatorIS5_EEEEP8_jobject\",\n            void, RunBackgroundVerification,\n            (void * thiz, const std::vector<const void *> &dex_files,\n                    jobject class_loader), {\n                if (lspd::Context::GetInstance()->GetCurrentClassLoader() == nullptr) {\n                    LOGD(\"Disabled background verification\");\n                    return;\n                }\n                backup(thiz, dex_files, class_loader);\n            });\n\n\n    static void DisableBackgroundVerification(const lsplant::HookHandler &handler) {\n        const int api_level = lspd::GetAndroidApiLevel();\n        if (api_level >= __ANDROID_API_Q__) {\n            HookSyms(handler, RunBackgroundVerificationWithContext, RunBackgroundVerification);\n        }\n    }\n}\n\n\n#endif //LSPATCH_OAT_FILE_MANAGER_H\n"
  },
  {
    "path": "patch-loader/src/main/jni/src/config_impl.h",
    "content": "/*\n * This file is part of LSPosed.\n *\n * LSPosed is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * LSPosed is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n *\n * Copyright (C) 2022 LSPosed Contributors\n */\n\n//\n// Created by Nullptr on 2022/5/11.\n//\n\n#pragma once\n\n#include <string>\n#include \"config_bridge.h\"\n\nnamespace lspd {\n\n    class ConfigImpl : public ConfigBridge {\n    public:\n        inline static void Init() {\n            instance_ = std::make_unique<ConfigImpl>();\n        }\n\n        virtual obfuscation_map_t& obfuscation_map() override {\n            return obfuscation_map_;\n        }\n\n        virtual void obfuscation_map(obfuscation_map_t m) override {\n            obfuscation_map_ = std::move(m);\n        }\n\n    private:\n        inline static std::map<std::string, std::string> obfuscation_map_ = {\n                {\"de.robv.android.xposed.\", \"de.robv.android.xposed.\"},\n                { \"android.app.AndroidApp\", \"android.app.AndroidApp\"},\n                { \"android.content.res.XRes\", \"android.content.res.XRes\"},\n                { \"android.content.res.XModule\", \"android.content.res.XModule\"},\n                { \"org.lsposed.lspd.core.\", \"org.lsposed.lspd.core.\"},\n                { \"org.lsposed.lspd.nativebridge.\", \"org.lsposed.lspd.nativebridge.\"},\n                { \"org.lsposed.lspd.service.\", \"org.lsposed.lspd.service.\"},\n        };\n    };\n}\n\n"
  },
  {
    "path": "patch-loader/src/main/jni/src/jni/bypass_sig.cpp",
    "content": "//\n// Created by VIP on 2021/4/25.\n//\n\n#include \"bypass_sig.h\"\n#include \"elf_util.h\"\n#include \"logging.h\"\n#include \"native_util.h\"\n#include \"patch_loader.h\"\n#include \"utils/hook_helper.hpp\"\n#include \"utils/jni_helper.hpp\"\n\nnamespace lspd {\n\n    std::string apkPath;\n    std::string redirectPath;\n\n    CREATE_HOOK_STUB_ENTRY(\n            \"__openat\",\n            int, __openat,\n            (int fd, const char* pathname, int flag, int mode), {\n                if (pathname == apkPath) {\n                    LOGD(\"redirect openat\");\n                    return backup(fd, redirectPath.c_str(), flag, mode);\n                }\n                return backup(fd, pathname, flag, mode);\n            });\n\n    LSP_DEF_NATIVE_METHOD(void, SigBypass, enableOpenatHook, jstring origApkPath, jstring cacheApkPath) {\n        auto sym_openat = SandHook::ElfImg(\"libc.so\").getSymbAddress<void *>(\"__openat\");\n        auto r = HookSymNoHandle(handler, sym_openat, __openat);\n        if (!r) {\n            LOGE(\"Hook __openat fail\");\n            return;\n        }\n        lsplant::JUTFString str1(env, origApkPath);\n        lsplant::JUTFString str2(env, cacheApkPath);\n        apkPath = str1.get();\n        redirectPath = str2.get();\n        LOGD(\"apkPath %s\", apkPath.c_str());\n        LOGD(\"redirectPath %s\", redirectPath.c_str());\n    }\n\n    static JNINativeMethod gMethods[] = {\n            LSP_NATIVE_METHOD(SigBypass, enableOpenatHook, \"(Ljava/lang/String;Ljava/lang/String;)V\")\n    };\n\n    void RegisterBypass(JNIEnv* env) {\n        REGISTER_LSP_NATIVE_METHODS(SigBypass);\n    }\n}\n"
  },
  {
    "path": "patch-loader/src/main/jni/src/jni/bypass_sig.h",
    "content": "#pragma once\n\n#include <jni.h>\n\nnamespace lspd {\n\n    void RegisterBypass(JNIEnv* env);\n\n} // namespace lspd\n"
  },
  {
    "path": "patch-loader/src/main/jni/src/patch_loader.cpp",
    "content": "/*\n * This file is part of LSPosed.\n *\n * LSPosed is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * LSPosed is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n *\n * Copyright (C) 2022 LSPosed Contributors\n */\n\n//\n// Created by Nullptr on 2022/3/17.\n//\n\n#include \"art/runtime/oat_file_manager.h\"\n#include \"art/runtime/jit/profile_saver.h\"\n#include \"elf_util.h\"\n#include \"jni/bypass_sig.h\"\n#include \"native_util.h\"\n#include \"patch_loader.h\"\n#include \"symbol_cache.h\"\n#include \"utils/jni_helper.hpp\"\n\nusing namespace lsplant;\n\nnamespace lspd {\n\n    void PatchLoader::LoadDex(JNIEnv* env, Context::PreloadedDex&& dex) {\n        auto class_activity_thread = JNI_FindClass(env, \"android/app/ActivityThread\");\n        auto class_activity_thread_app_bind_data = JNI_FindClass(env, \"android/app/ActivityThread$AppBindData\");\n        auto class_loaded_apk = JNI_FindClass(env, \"android/app/LoadedApk\");\n\n        auto mid_current_activity_thread = JNI_GetStaticMethodID(env, class_activity_thread, \"currentActivityThread\",\n                                                                 \"()Landroid/app/ActivityThread;\");\n        auto mid_get_classloader = JNI_GetMethodID(env, class_loaded_apk, \"getClassLoader\", \"()Ljava/lang/ClassLoader;\");\n        auto fid_m_bound_application = JNI_GetFieldID(env, class_activity_thread, \"mBoundApplication\",\n                                                      \"Landroid/app/ActivityThread$AppBindData;\");\n        auto fid_info = JNI_GetFieldID(env, class_activity_thread_app_bind_data, \"info\", \"Landroid/app/LoadedApk;\");\n\n        auto activity_thread = JNI_CallStaticObjectMethod(env, class_activity_thread, mid_current_activity_thread);\n        auto m_bound_application = JNI_GetObjectField(env, activity_thread, fid_m_bound_application);\n        auto info = JNI_GetObjectField(env, m_bound_application, fid_info);\n        auto stub_classloader = JNI_CallObjectMethod(env, info, mid_get_classloader);\n\n        if (!stub_classloader) [[unlikely]] {\n            LOGE(\"getStubClassLoader failed!!!\");\n            return;\n        }\n\n        auto in_memory_classloader = JNI_FindClass(env, \"dalvik/system/InMemoryDexClassLoader\");\n        auto mid_init = JNI_GetMethodID(env, in_memory_classloader, \"<init>\",\n                                        \"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V\");\n        auto byte_buffer_class = JNI_FindClass(env, \"java/nio/ByteBuffer\");\n        auto dex_buffer = env->NewDirectByteBuffer(dex.data(), dex.size());\n        if (auto my_cl = JNI_NewObject(env, in_memory_classloader, mid_init, dex_buffer, stub_classloader)) {\n            inject_class_loader_ = JNI_NewGlobalRef(env, my_cl);\n        } else {\n            LOGE(\"InMemoryDexClassLoader creation failed!!!\");\n            return;\n        }\n\n        env->DeleteLocalRef(dex_buffer);\n    }\n\n    void PatchLoader::InitArtHooker(JNIEnv* env, const InitInfo& initInfo) {\n        Context::InitArtHooker(env, initInfo);\n        handler = initInfo;\n        art::DisableInline(initInfo);\n        art::DisableBackgroundVerification(initInfo);\n    }\n\n    void PatchLoader::InitHooks(JNIEnv* env) {\n        Context::InitHooks(env);\n        RegisterBypass(env);\n    }\n\n    void PatchLoader::SetupEntryClass(JNIEnv* env) {\n        if (auto entry_class = FindClassFromLoader(env, GetCurrentClassLoader(),\n                                                   \"org.lsposed.lspatch.loader.LSPApplication\")) {\n            entry_class_ = JNI_NewGlobalRef(env, entry_class);\n        }\n    }\n\n    void PatchLoader::Load(JNIEnv* env) {\n        InitSymbolCache(nullptr);\n        lsplant::InitInfo initInfo {\n                .inline_hooker = [](auto t, auto r) {\n                    void* bk = nullptr;\n                    return HookFunction(t, r, &bk) == RS_SUCCESS ? bk : nullptr;\n                },\n                .inline_unhooker = [](auto t) {\n                    return UnhookFunction(t) == RT_SUCCESS;\n                },\n                .art_symbol_resolver = [](auto symbol) {\n                    return GetArt()->getSymbAddress<void*>(symbol);\n                },\n                .art_symbol_prefix_resolver = [](auto symbol) {\n                    return GetArt()->getSymbPrefixFirstAddress(symbol);\n                },\n        };\n\n        auto stub = JNI_FindClass(env, \"org/lsposed/lspatch/metaloader/LSPAppComponentFactoryStub\");\n        auto dex_field = JNI_GetStaticFieldID(env, stub, \"dex\", \"[B\");\n\n        ScopedLocalRef<jbyteArray> array = JNI_GetStaticObjectField(env, stub, dex_field);\n        auto dex = PreloadedDex {env->GetByteArrayElements(array.get(), nullptr), static_cast<size_t>(JNI_GetArrayLength(env, array))};\n\n        InitArtHooker(env, initInfo);\n        LoadDex(env, std::move(dex));\n        InitHooks(env);\n\n        GetArt(true);\n\n        SetupEntryClass(env);\n        FindAndCall(env, \"onLoad\", \"()V\");\n    }\n} // namespace lspd\n"
  },
  {
    "path": "patch-loader/src/main/jni/src/patch_loader.h",
    "content": "/*\n * This file is part of LSPosed.\n *\n * LSPosed is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * LSPosed is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n *\n * Copyright (C) 2022 LSPosed Contributors\n */\n\n//\n// Created by Nullptr on 2022/3/17.\n//\n\n#pragma once\n\n#include \"context.h\"\n\nnamespace lspd {\n\n    inline lsplant::InitInfo handler;\n\n    class PatchLoader : public Context {\n    public:\n        inline static void Init() {\n            instance_ = std::make_unique<PatchLoader>();\n        }\n\n        inline static PatchLoader* GetInstance() {\n            return static_cast<PatchLoader*>(instance_.get());\n        }\n\n        void Load(JNIEnv* env);\n\n    protected:\n        void InitArtHooker(JNIEnv* env, const lsplant::InitInfo& initInfo) override;\n\n        void InitHooks(JNIEnv* env) override;\n\n        void LoadDex(JNIEnv* env, PreloadedDex&& dex) override;\n\n        void SetupEntryClass(JNIEnv* env) override;\n    };\n} // namespace lspd\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "enableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n\npluginManagement {\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n    }\n}\n\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        mavenLocal {\n            content {\n                includeGroup(\"io.github.libxposed\")\n            }\n        }\n    }\n    versionCatalogs {\n        create(\"libs\") {\n            from(files(\"core/gradle/libs.versions.toml\"))\n        }\n        create(\"lspatch\") {\n            from(files(\"gradle/lspatch.versions.toml\"))\n        }\n    }\n}\n\nrootProject.name = \"LSPatch\"\ninclude(\n    \":apkzlib\",\n    \":core\",\n    \":hiddenapi:bridge\",\n    \":hiddenapi:stubs\",\n    \":jar\",\n    \":manager\",\n    \":meta-loader\",\n    \":patch\",\n    \":patch-loader\",\n    \":services:daemon-service\",\n    \":services:manager-service\",\n    \":services:xposed-service:interface\",\n    \":share:android\",\n    \":share:java\",\n)\n\nproject(\":core\").projectDir = file(\"core/core\")\nproject(\":hiddenapi:bridge\").projectDir = file(\"core/hiddenapi/bridge\")\nproject(\":hiddenapi:stubs\").projectDir = file(\"core/hiddenapi/stubs\")\nproject(\":services:daemon-service\").projectDir = file(\"core/services/daemon-service\")\nproject(\":services:manager-service\").projectDir = file(\"core/services/manager-service\")\nproject(\":services:xposed-service:interface\").projectDir = file(\"core/services/xposed-service/interface\")\n\nbuildCache { local { removeUnusedEntriesAfterDays = 1 } }\n"
  },
  {
    "path": "share/android/.gitignore",
    "content": "/build"
  },
  {
    "path": "share/android/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.agp.lib)\n}\n\nandroid {\n    namespace = \"org.lsposed.lspatch.share\"\n\n    buildFeatures {\n        androidResources = false\n        buildConfig = false\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"))\n        }\n    }\n}\n\ndependencies {\n    implementation(projects.services.daemonService)\n}\n"
  },
  {
    "path": "share/android/src/main/java/org/lsposed/lspatch/util/ModuleLoader.java",
    "content": "package org.lsposed.lspatch.util;\n\nimport android.os.SharedMemory;\nimport android.system.ErrnoException;\nimport android.system.OsConstants;\nimport android.util.Log;\n\nimport org.lsposed.lspd.models.PreLoadedApk;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.channels.Channels;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.zip.ZipFile;\n\npublic class ModuleLoader {\n\n    private static final String TAG = \"LSPatch\";\n\n    private static void readDexes(ZipFile apkFile, List<SharedMemory> preLoadedDexes) {\n        int secondary = 2;\n        for (var dexFile = apkFile.getEntry(\"classes.dex\"); dexFile != null;\n             dexFile = apkFile.getEntry(\"classes\" + secondary + \".dex\"), secondary++) {\n            try (var in = apkFile.getInputStream(dexFile)) {\n                var memory = SharedMemory.create(null, in.available());\n                var byteBuffer = memory.mapReadWrite();\n                Channels.newChannel(in).read(byteBuffer);\n                SharedMemory.unmap(byteBuffer);\n                memory.setProtect(OsConstants.PROT_READ);\n                preLoadedDexes.add(memory);\n            } catch (IOException | ErrnoException e) {\n                Log.w(TAG, \"Can not load \" + dexFile + \" in \" + apkFile, e);\n            }\n        }\n    }\n\n    private static void readName(ZipFile apkFile, String initName, List<String> names) {\n        var initEntry = apkFile.getEntry(initName);\n        if (initEntry == null) return;\n        try (var in = apkFile.getInputStream(initEntry)) {\n            var reader = new BufferedReader(new InputStreamReader(in));\n            String name;\n            while ((name = reader.readLine()) != null) {\n                name = name.trim();\n                if (name.isEmpty() || name.startsWith(\"#\")) continue;\n                names.add(name);\n            }\n        } catch (IOException e) {\n            Log.e(TAG, \"Can not open \" + initEntry, e);\n        }\n    }\n\n    public static PreLoadedApk loadModule(String path) {\n        if (path == null) return null;\n        var file = new PreLoadedApk();\n        var preLoadedDexes = new ArrayList<SharedMemory>();\n        var moduleClassNames = new ArrayList<String>(1);\n        var moduleLibraryNames = new ArrayList<String>(1);\n        try (var apkFile = new ZipFile(path)) {\n            readDexes(apkFile, preLoadedDexes);\n            readName(apkFile, \"assets/xposed_init\", moduleClassNames);\n            readName(apkFile, \"assets/native_init\", moduleLibraryNames);\n        } catch (IOException e) {\n            Log.e(TAG, \"Can not open \" + path, e);\n            return null;\n        }\n        if (preLoadedDexes.isEmpty()) return null;\n        if (moduleClassNames.isEmpty()) return null;\n        file.preLoadedDexes = preLoadedDexes;\n        file.moduleClassNames = moduleClassNames;\n        file.moduleLibraryNames = moduleLibraryNames;\n        return file;\n    }\n}\n"
  },
  {
    "path": "share/java/.gitignore",
    "content": "/build"
  },
  {
    "path": "share/java/build.gradle.kts",
    "content": "val apiCode: Int by rootProject.extra\nval verCode: Int by rootProject.extra\nval verName: String by rootProject.extra\nval coreVerCode: Int by rootProject.extra\nval coreVerName: String by rootProject.extra\nval androidSourceCompatibility: JavaVersion by rootProject.extra\nval androidTargetCompatibility: JavaVersion by rootProject.extra\n\nplugins {\n    id(\"java-library\")\n}\n\njava {\n    sourceCompatibility = androidSourceCompatibility\n    targetCompatibility = androidTargetCompatibility\n}\n\nval generateTask = task<Copy>(\"generateJava\") {\n    val template = mapOf(\n        \"apiCode\" to apiCode,\n        \"verCode\" to verCode,\n        \"verName\" to verName,\n        \"coreVerCode\" to coreVerCode,\n        \"coreVerName\" to coreVerName\n    )\n    inputs.properties(template)\n    from(\"src/template/java\")\n    into(\"$buildDir/generated/java\")\n    expand(template)\n}\n\nsourceSets[\"main\"].java.srcDir(\"$buildDir/generated/java\")\ntasks[\"compileJava\"].dependsOn(generateTask)\n"
  },
  {
    "path": "share/java/src/main/java/org/lsposed/lspatch/share/Constants.java",
    "content": "package org.lsposed.lspatch.share;\n\npublic class Constants {\n\n    final static public String CONFIG_ASSET_PATH = \"assets/lspatch/config.json\";\n    final static public String LOADER_DEX_ASSET_PATH = \"assets/lspatch/loader.dex\";\n    final static public String META_LOADER_DEX_ASSET_PATH = \"assets/lspatch/metaloader.dex\";\n    final static public String ORIGINAL_APK_ASSET_PATH = \"assets/lspatch/origin.apk\";\n    final static public String EMBEDDED_MODULES_ASSET_PATH = \"assets/lspatch/modules/\";\n\n    final static public String PATCH_FILE_SUFFIX = \"-lspatched.apk\";\n    final static public String PROXY_APP_COMPONENT_FACTORY = \"org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub\";\n    final static public String MANAGER_PACKAGE_NAME = \"org.lsposed.lspatch\";\n    final static public int MIN_ROLLING_VERSION_CODE = 348;\n\n    final static public int SIGBYPASS_LV_DISABLE = 0;\n    final static public int SIGBYPASS_LV_PM = 1;\n    final static public int SIGBYPASS_LV_PM_OPENAT = 2;\n    final static public int SIGBYPASS_LV_MAX = 3;\n}\n"
  },
  {
    "path": "share/java/src/main/java/org/lsposed/lspatch/share/PatchConfig.java",
    "content": "package org.lsposed.lspatch.share;\n\npublic class PatchConfig {\n\n    public final boolean useManager;\n    public final boolean debuggable;\n    public final boolean overrideVersionCode;\n    public final int sigBypassLevel;\n    public final String originalSignature;\n    public final String appComponentFactory;\n    public final LSPConfig lspConfig;\n\n    public PatchConfig(\n            boolean useManager,\n            boolean debuggable,\n            boolean overrideVersionCode,\n            int sigBypassLevel,\n            String originalSignature,\n            String appComponentFactory\n    ) {\n        this.useManager = useManager;\n        this.debuggable = debuggable;\n        this.overrideVersionCode = overrideVersionCode;\n        this.sigBypassLevel = sigBypassLevel;\n        this.originalSignature = originalSignature;\n        this.appComponentFactory = appComponentFactory;\n        this.lspConfig = LSPConfig.instance;\n    }\n}\n"
  },
  {
    "path": "share/java/src/template/java/org.lsposed.lspatch.share/LSPConfig.java",
    "content": "package org.lsposed.lspatch.share;\n\npublic class LSPConfig {\n\n    public static final LSPConfig instance;\n\n    public int API_CODE;\n    public int VERSION_CODE;\n    public String VERSION_NAME;\n    public int CORE_VERSION_CODE;\n    public String CORE_VERSION_NAME;\n\n    private LSPConfig() {\n    }\n\n    static {\n        instance = new LSPConfig();\n        instance.API_CODE = ${apiCode};\n        instance.VERSION_CODE = ${verCode};\n        instance.VERSION_NAME = \"${verName}\";\n        instance.CORE_VERSION_CODE = ${coreVerCode};\n        instance.CORE_VERSION_NAME = \"${coreVerName}\";\n    }\n}\n"
  }
]