[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 反馈 Bug\r\ndescription: 反馈遇到的问题\r\nlabels: [bug]\r\ntitle: \"[Bug] \"\r\nbody:\r\n  - type: markdown\r\n    attributes:\r\n      value: |\r\n        为了使我们更好地帮助你，请提供以下信息。\r\n  - type: textarea\r\n    id: desc\r\n    attributes:\r\n      label: 问题描述\r\n      description: 发生了什么情况？有什么现状？\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: steps\r\n    attributes:\r\n      label: 复现步骤\r\n      description: 如何复现\r\n      placeholder: |\r\n        1. 打开...\r\n        2. 点击...\r\n        3. 出现...状况\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: expected\r\n    attributes:\r\n      label: 预期行为\r\n      description: 正常情况下应该发生什么\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: actual\r\n    attributes:\r\n      label: 实际行为\r\n      description: 实际上发生了什么\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: media\r\n    attributes:\r\n      label: 截图或录屏\r\n      description: 问题复现时候的截图或录屏\r\n      placeholder: 点击文本框下面小长条可以上传文件\r\n  - type: input\r\n    id: android-ver\r\n    attributes:\r\n      label: 安卓版本\r\n      placeholder: \"12\"\r\n    validations:\r\n      required: true\r\n  - type: input\r\n    id: romaing-ver\r\n    attributes:\r\n      label: 哔哩漫游版本\r\n      placeholder: 1.6.2\r\n    validations:\r\n      required: true\r\n  - type: dropdown\r\n    id: client\r\n    attributes:\r\n      label: 哔哩哔哩版本\r\n      options:\r\n        - 粉版（普通版）\r\n        - 概念版\r\n        - HD 版\r\n        - play 版\r\n      description: 目前仅支持粉版、概念版、HD 版和 play 版\r\n    validations:\r\n      required: true\r\n  - type: input\r\n    id: client-ver\r\n    attributes:\r\n      label: 哔哩哔哩版本号\r\n      description: 非最新版本可能不受理\r\n      placeholder: 6.74.0\r\n    validations:\r\n      required: true\r\n  - type: input\r\n    id: framework\r\n    attributes:\r\n      label: 使用的框架和版本\r\n      placeholder: LSPosed 1.8.2\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: misc\r\n    attributes:\r\n      label: 其他\r\n      description: 如哪部番、Magisk 版本等\r\n  - type: textarea\r\n    id: logs\r\n    attributes:\r\n      label: 日志\r\n      description: 请使用漫游自带的导出日志功能或者使用 `adb logcat`。无日志提交会被关闭。\r\n      placeholder: 点击文本框下面小长条可以上传文件\r\n    validations:\r\n      required: true\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\r\ncontact_links:\r\n  - name: Telegram 频道\r\n    url: https://t.me/biliroaming\r\n    about: 可以订阅更新、讨论交流\r\n  - name: QQ 频道\r\n    url: https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&inviteCode=NVoD5&from=246610&biz=ka\r\n    about: 可以订阅更新、讨论交流"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 功能请求\r\ndescription: 想要请求添加某个功能\r\nlabels: [enhancement]\r\ntitle: \"[Feature] \"\r\nbody:\r\n  - type: textarea\r\n    id: reason\r\n    attributes:\r\n      label: 原因\r\n      description: 为什么想要这个功能\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: desc\r\n    attributes:\r\n      label: 功能简述\r\n      description: 想要个怎样的功能\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: logic\r\n    attributes:\r\n      label: 功能逻辑\r\n      description: 如何互交、如何使用等\r\n    validations:\r\n      required: true\r\n  - type: textarea\r\n    id: ref\r\n    attributes:\r\n      label: 实现参考\r\n      description: 该功能可能的实现方式，或者其他已经实现该功能的应用等\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new_server.yml",
    "content": "name: 添加公共服务器\r\ndescription: 把自己的服务器添加到公共服务器列表\r\nlabels: [server]\r\ntitle: \"[Server] \"\r\nbody:\r\n  - type: input\r\n    id: contact\r\n    attributes:\r\n      label: 联系方式\r\n      description: Telegram、Github 账号等联系方式\r\n    validations:\r\n      required: true\r\n  - type: input\r\n    id: domain\r\n    attributes:\r\n      label: 域名\r\n      description: 服务器域名\r\n    validations:\r\n      required: true\r\n  - type: checkboxes\r\n    id: areas\r\n    attributes:\r\n      label: 支持地区\r\n      description: 服务器支持解析的地区\r\n      options:\r\n        - label: 中国大陆\r\n        - label: 港澳\r\n        - label: 台湾\r\n        - label: 东南亚\r\n  - type: checkboxes\r\n    id: premium\r\n    attributes:\r\n      label: 带会员专享\r\n      options:\r\n        - label: 服务器只有带会员用户能解析\r\n  - type: input\r\n    id: sponsor\r\n    attributes:\r\n      label: 赞助地址\r\n      description: 爱发电等\r\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: gradle\n    directory: \"/\"\n    schedule:\n      interval: daily\n      time: \"21:00\"\n    open-pull-requests-limit: 10\n    target-branch: master\n    registries:\n      - maven-google\n      - gralde-plugin\n    groups:\n      maven-dependencies:\n        patterns:\n          - \"*\"\nregistries:\n  maven-google:\n    type: maven-repository\n    url: \"https://dl.google.com/dl/android/maven2/\"\n  gralde-plugin:\n    type: maven-repository\n    url: \"https://plugins.gradle.org/m2/\"\n"
  },
  {
    "path": ".github/workflows/PR.yml",
    "content": "name: PR Build\n\non: [pull_request]\n\njobs:\n  build:\n    name: Build on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    env:\n      CCACHE_DIR: ${{ github.workspace }}/.ccache\n      CCACHE_COMPILERCHECK: \"%compiler% -dumpmachine; %compiler% -dumpversion\"\n      CCACHE_NOHASHDIR: true\n      CCACHE_MAXSIZE: 1G\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ ubuntu-latest ]\n\n    steps:\n    - name: Check out\n      uses: actions/checkout@v3\n      with:\n        submodules: 'recursive'\n        fetch-depth: 0\n    - name: Set up JDK 17\n      uses: actions/setup-java@v3\n      with:\n        distribution: 'temurin'\n        java-version: '17'\n        cache: 'gradle'\n    - name: Set up ccache\n      uses: hendrikmuhs/ccache-action@v1.2\n      with:\n        key: ${{ runner.os }}-${{ github.sha }}\n        restore-keys: ${{ runner.os }}\n    - name: Build with Gradle\n      run: |\n        echo 'org.gradle.caching=true' >> gradle.properties\n        echo 'org.gradle.parallel=true' >> gradle.properties\n        echo 'org.gradle.vfs.watch=true' >> gradle.properties\n        echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties\n        echo 'android.native.buildOutput=verbose' >> gradle.properties\n        ./gradlew assemble\n    - name: Stop gradle daemon\n      run: ./gradlew --stop\n    - name: Upload build artifact\n      uses: actions/upload-artifact@v4\n      with:\n        name: ${{ matrix.os }}-artifact\n        path: |\n          app/build/outputs\n          app/release\n"
  },
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Android CI\n\non:\n  push:\n    branches: [ master ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    env:\n      CCACHE_DIR: ${{ github.workspace }}/.ccache\n      CCACHE_COMPILERCHECK: \"%compiler% -dumpmachine; %compiler% -dumpversion\"\n      CCACHE_NOHASHDIR: true\n      CCACHE_MAXSIZE: 1G\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          submodules: 'recursive'\n          fetch-depth: 0\n      - name: Setup JDK 17\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'temurin'\n          java-version: 17\n          cache: 'gradle'\n      - name: Retrieve version\n        run: |\n          echo VERSION=$(echo ${{ github.event.head_commit.id }} | head -c 10) >> $GITHUB_ENV\n      - name: Set up ccache\n        uses: hendrikmuhs/ccache-action@v1.2\n        with:\n          key: ${{ runner.os }}-${{ github.sha }}\n          restore-keys: ${{ runner.os }}\n      - name: Write key\n        if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/master' ) || github.ref_type == 'tag' }}\n        run: |\n          if [ ! -z \"${{ secrets.SIGNING_KEY }}\" ]; then\n            echo 'Writing sign key'\n            echo releaseStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties\n            echo releaseKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties\n            echo releaseKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties\n            echo releaseStoreFile='key.jks' >> gradle.properties\n            echo '${{ secrets.SIGNING_KEY }}' | base64 --decode > key.jks\n          fi\n      - name: Build with Gradle\n        run: |\n          echo 'org.gradle.caching=true' >> gradle.properties\n          echo 'org.gradle.parallel=true' >> gradle.properties\n          echo 'org.gradle.vfs.watch=true' >> gradle.properties\n          echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties\n          echo 'android.native.buildOutput=verbose' >> gradle.properties\n          ./gradlew -PappVerName=${{ env.VERSION }} assembleRelease assembleDebug\n      - name: Upload built apk\n        if: success()\n        uses: actions/upload-artifact@v4\n        with:\n          name: snapshot\n          path: |\n              app/build/outputs/apk\n              app/build/outputs/mapping\n              app/release\n      - name: Post to channel\n        if: github.ref == 'refs/heads/master'\n        env:\n          CHANNEL_ID: ${{ secrets.TELEGRAM_TO }}\n          BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}\n          FILE: app/release/BiliRoaming_${{ env.VERSION }}.apk\n          COMMIT_MESSAGE: |+\n            New push to github\\!\n            ```\n            ${{ github.event.head_commit.message }}\n            ```by `${{ github.event.head_commit.author.name }}`\n            See commit detail [here](${{ github.event.head_commit.url }})\n            Snapshot apk is attached \\(unsupported by TAICHI\\)\n        run: |\n          ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ[\"COMMIT_MESSAGE\"])))'`\n          curl -v \"https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22:%22document%22,%20%22media%22:%22attach://release%22,%22parse_mode%22:%22MarkdownV2%22,%22caption%22:${ESCAPED}%7D%5D\"  -F release=\"@$FILE\"\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n.idea\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n*.apk\n*.jar\n/.kotlin\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"app/src/main/jni/dex_builder\"]\n\tpath = app/src/main/jni/dex_builder\n\turl = git@github.com:LSPosed/DexBuilder.git\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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    {project}  Copyright (C) {year}  {fullname}\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<http://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<http://www.gnu.org/philosophy/why-not-lgpl.html>."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<img width=\"160\" src=\"imgs/icon.png\" alt=\"logo\">\n \nLogo来自[哔哩哔哩漫游娘](https://www.weibo.com/p/230418139a6f1100102vlj6)\n\n[![Android CI](https://github.com/yujincheng08/BiliRoaming/workflows/Android%20CI/badge.svg)](https://github.com/yujincheng08/BiliRoaming/actions)\n[![Chat](https://img.shields.io/badge/Join-QQ%E9%A2%91%E9%81%93-red?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjEgMTQzLjg5Ij48cGF0aCBmaWxsPSIjZmFhYjA3IiBkPSJNNjAuNTAzIDE0Mi4yMzdjLTEyLjUzMyAwLTI0LjAzOC00LjE5NS0zMS40NDUtMTAuNDYtMy43NjIgMS4xMjQtOC41NzQgMi45MzItMTEuNjEgNS4xNzUtMi42IDEuOTE4LTIuMjc1IDMuODc0LTEuODA3IDQuNjYzIDIuMDU2IDMuNDcgMzUuMjczIDIuMjE2IDQ0Ljg2MiAxLjEzNnptMCAwYzEyLjUzNSAwIDI0LjAzOS00LjE5NSAzMS40NDctMTAuNDYgMy43NiAxLjEyNCA4LjU3MyAyLjkzMiAxMS42MSA1LjE3NSAyLjU5OCAxLjkxOCAyLjI3NCAzLjg3NCAxLjgwNSA0LjY2My0yLjA1NiAzLjQ3LTM1LjI3MiAyLjIxNi00NC44NjIgMS4xMzZ6bTAgMCIvPjxwYXRoIGQ9Ik02MC41NzYgNjcuMTE5YzIwLjY5OC0uMTQgMzcuMjg2LTQuMTQ3IDQyLjkwNy01LjY4MyAxLjM0LS4zNjcgMi4wNTYtMS4wMjQgMi4wNTYtMS4wMjQuMDA1LS4xODkuMDg1LTMuMzcuMDg1LTUuMDFDMTA1LjYyNCAyNy43NjggOTIuNTguMDAxIDYwLjUgMCAyOC40Mi4wMDEgMTUuMzc1IDI3Ljc2OSAxNS4zNzUgNTUuNDAxYzAgMS42NDIuMDggNC44MjIuMDg2IDUuMDEgMCAwIC41ODMuNjE1IDEuNjUuOTEzIDUuMTkgMS40NDQgMjIuMDkgNS42NSA0My4zMTIgNS43OTV6bTU2LjI0NSAyMy4wMmMtMS4yODMtNC4xMjktMy4wMzQtOC45NDQtNC44MDgtMTMuNTY4IDAgMC0xLjAyLS4xMjYtMS41MzcuMDIzLTE1LjkxMyA0LjYyMy0zNS4yMDIgNy41Ny00OS45IDcuMzkyaC0uMTUzYy0xNC42MTYuMTc1LTMzLjc3NC0yLjczNy00OS42MzQtNy4zMTUtLjYwNi0uMTc1LTEuODAyLS4xLTEuODAyLS4xLTEuNzc0IDQuNjI0LTMuNTI1IDkuNDQtNC44MDggMTMuNTY4LTYuMTE5IDE5LjY5LTQuMTM2IDI3LjgzOC0yLjYyNyAyOC4wMiAzLjIzOS4zOTIgMTIuNjA2LTE0LjgyMSAxMi42MDYtMTQuODIxIDAgMTUuNDU5IDEzLjk1NyAzOS4xOTUgNDUuOTE4IDM5LjQxM2guODQ4YzMxLjk2LS4yMTggNDUuOTE3LTIzLjk1NCA0NS45MTctMzkuNDEzIDAgMCA5LjM2OCAxNS4yMTMgMTIuNjA3IDE0LjgyMiAxLjUwOC0uMTgzIDMuNDkxLTguMzMyLTIuNjI3LTI4LjAyMSIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00OS4wODUgNDAuODI0Yy00LjM1Mi4xOTctOC4wNy00Ljc2LTguMzA0LTExLjA2My0uMjM2LTYuMzA1IDMuMDk4LTExLjU3NiA3LjQ1LTExLjc3MyA0LjM0Ny0uMTk1IDguMDY0IDQuNzYgOC4zIDExLjA2NS4yMzggNi4zMDYtMy4wOTcgMTEuNTc3LTcuNDQ2IDExLjc3MW0zMS4xMzMtMTEuMDYzYy0uMjMzIDYuMzAyLTMuOTUxIDExLjI2LTguMzAzIDExLjA2My00LjM1LS4xOTUtNy42ODQtNS40NjUtNy40NDYtMTEuNzcuMjM2LTYuMzA1IDMuOTUyLTExLjI2IDguMy0xMS4wNjYgNC4zNTIuMTk3IDcuNjg2IDUuNDY4IDcuNDQ5IDExLjc3MyIvPjxwYXRoIGZpbGw9IiNmYWFiMDciIGQ9Ik04Ny45NTIgNDkuNzI1Qzg2Ljc5IDQ3LjE1IDc1LjA3NyA0NC4yOCA2MC41NzggNDQuMjhoLS4xNTZjLTE0LjUgMC0yNi4yMTIgMi44Ny0yNy4zNzUgNS40NDZhLjg2My44NjMgMCAwMC0uMDg1LjM2Ny44OC44OCAwIDAwLjE2LjQ5NmMuOTggMS40MjcgMTMuOTg1IDguNDg3IDI3LjMgOC40ODdoLjE1NmMxMy4zMTQgMCAyNi4zMTktNy4wNTggMjcuMjk5LTguNDg3YS44NzMuODczIDAgMDAuMTYtLjQ5OC44NTYuODU2IDAgMDAtLjA4NS0uMzY1Ii8+PHBhdGggZD0iTTU0LjQzNCAyOS44NTRjLjE5OSAyLjQ5LTEuMTY3IDQuNzAyLTMuMDQ2IDQuOTQzLTEuODgzLjI0Mi0zLjU2OC0xLjU4LTMuNzY4LTQuMDctLjE5Ny0yLjQ5MiAxLjE2Ny00LjcwNCAzLjA0My00Ljk0NCAxLjg4Ni0uMjQ0IDMuNTc0IDEuNTggMy43NzEgNC4wN20xMS45NTYuODMzYy4zODUtLjY4OSAzLjAwNC00LjMxMiA4LjQyNy0yLjk5MyAxLjQyNS4zNDcgMi4wODQuODU3IDIuMjIzIDEuMDU3LjIwNS4yOTYuMjYyLjcxOC4wNTMgMS4yODYtLjQxMiAxLjEyNi0xLjI2MyAxLjA5NS0xLjczNC44NzUtLjMwNS0uMTQyLTQuMDgyLTIuNjYtNy41NjIgMS4wOTctLjI0LjI1Ny0uNjY4LjM0Ni0xLjA3My4wNC0uNDA3LS4zMDgtLjU3NC0uOTMtLjMzNC0xLjM2MiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02MC41NzYgODMuMDhoLS4xNTNjLTkuOTk2LjEyLTIyLjExNi0xLjIwNC0zMy44NTQtMy41MTgtMS4wMDQgNS44MTgtMS42MSAxMy4xMzItMS4wOSAyMS44NTMgMS4zMTYgMjIuMDQzIDE0LjQwNyAzNS45IDM0LjYxNCAzNi4xaC44MmMyMC4yMDgtLjIgMzMuMjk4LTE0LjA1NyAzNC42MTYtMzYuMS41Mi04LjcyMy0uMDg3LTE2LjAzNS0xLjA5Mi0yMS44NTQtMTEuNzM5IDIuMzE1LTIzLjg2MiAzLjY0LTMzLjg2IDMuNTE4Ii8+PHBhdGggZmlsbD0iI2ViMTkyMyIgZD0iTTMyLjEwMiA4MS4yMzV2MjEuNjkzczkuOTM3IDIuMDA0IDE5Ljg5My42MTZWODMuNTM1Yy02LjMwNy0uMzU3LTEzLjEwOS0xLjE1Mi0xOS44OTMtMi4zIi8+PHBhdGggZmlsbD0iI2ViMTkyMyIgZD0iTTEwNS41MzkgNjAuNDEycy0xOS4zMyA2LjEwMi00NC45NjMgNi4yNzVoLS4xNTNjLTI1LjU5MS0uMTcyLTQ0Ljg5Ni02LjI1NS00NC45NjItNi4yNzVMOC45ODcgNzYuNTdjMTYuMTkzIDQuODgyIDM2LjI2MSA4LjAyOCA1MS40MzYgNy44NDVoLjE1M2MxNS4xNzUuMTgzIDM1LjI0Mi0yLjk2MyA1MS40MzctNy44NDV6bTAgMCIvPjwvc3ZnPg==)](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&inviteCode=NVoD5&from=246610&biz=ka)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/biliroaming)\n[![Download](https://img.shields.io/github/v/release/yujincheng08/biliroaming?color=critical&label=Downloads&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAF2UlEQVRIiaVWS2xTRxQ9d2bs5y+O4zgEYkgoShcESIG2EQJRREFAKmABRFCpbOiioumqrNiQCmjFiiB1Q6kqwR6k0NIKUBdFLBAt31BBoUqC8xEhgXwcO7bfezPVTJyQkBA+vdKT5fdm7rn3zL3nDilMtlt1dRiOx+G3bSjO4TIGSLkOrrtJMfYBgEoA0cKmAVKqHUR/EXBBSnmJ53JQHg9UIIDA06dY3NwMmoAgMI2NLZDAXuW6XwGogQaeahHFWIUCPiKlvgZjLVKI7wn4gdSLqYzaFC96oSJ612HsiqvUjwZsJlMKE5wvkV7vCVeIq4poEU0I/jlgKATzhMOAEADRZunx3FVEq15c/DpmwIlq80LcsYGthhnLArxe85DasMFEqT/0BAIb7oVCFy3GQFK+Bdxzk4xB2jbmSVkXFOI3WWBBdEmpKYRDNK8rGr3Iddr5vHk3TjPnsAcH4aTTsEpKwDwenQVkLodcXx9EOAzPrFlQrju+h7suyONBq8/366yBgYWW67YaSnuKi/EkGkVnWdkvOifvRDAiEGPIJJPwRqMoWbUKJISJXIMxvx+l69bBE4kg/egRSO8r7NU+NEteXbVCnBfDw+CpFPiemhpIzj8lxvZ5HGdyZoxhuK0NsdpaLG5sxNy6OqQePMBASwucTAbFK1Zg0YEDiK9ejZGuLgzcuQNvUdEkarlScBgryVhW+0godJvpKIjoWzZanZNo1FHHVq5EzdGjhkpzBsGgoU4pNUotYL4tPXIEpWvXIqMz5XzcjyoUEvd4vrOIwPyMrVZEFeqFvrGHhoyjJY2Nk4vBtk3mmr6JZ6Zt8cGD8CcSyPf3T3pPpnvUHJVOf8wcxrabs5qQmTsygv6bN1G+dSu43z9ps/D7IR3HPMLnm+yYc1Ts2oX8s2fTFS6Uz7dDuMCH42BCINvdDR4KoaqhAXO3bDHvc6kUnnZ0AJyjv70dVjhsMhzo6EDX/fsg10VxeTl8RUWILl9uisgUle6/Md9SwhVihQBRhVELzjHS3Y1AeTmqDx5EsKJifPFQMokLu3fDF4thTiyGcDxuziadTOJKQwNSnZ3YfOoUymtr4S0uNi2SevgQwfnzIXS7OM5o9SpVzj9fuvQb3Q0ymzXOlx8/bkAnWjAeR0Sf69WrCCUScHW0uuQtCyKZRM2ePajcscM41YWkqzdYWYnBlha46bQpNJOULvwxucv29qJs40b4Zs+eSj4R3tm3DyXr1yPV2mrYYEIg1daGotpaVO3fj4nirsHm19djyeHDUDq4QjIoiPegOVDbRmjBgmkPe8x0FfrmzEH28WOjMN5IBEsOHXrp+kh1tendbE/P2KsUg5SPUFAIO5OZEZAHAqbfck+eIN3aasD0mc1k4YULTTIY7fMuRkL8qXvQikTQcfnyjJu1hauqsOzYMSxrakJRzcyTS1umr8/QrRjT+nqdsWz2jEa3YjEM3LiB66dPv9JJfM0alOkp8wpLp9N42NyMoFYpzWI2e4Ypy7pMQnS4SiGeSCB58iT+aGpCX0cHpp/ZrzatP49u3cLvDQ3g/f3gWl+l7FFCXKKr9fX6z2fSsk5zIUC2ja72duRLShBMJEw1vskg1kE62SwybW3Q6htNJJB1XXhcdy9X6ie6tnOn4dj2+/9WjrNIEMHDGHLpNNLDw6as3xSQcY5wURG4ZSHrOGC53L/efL5K0yr8paWGX18+/8mAZbXpsaOVgfl8iLygo28CqgPNOQ7cYBBWMFjH9KDXzT9SWWkW6QnwJB6va6uuPq/n4v+9YuhRZ+dyqLSsbdFY7NzYZJlyL729bduWodLSZjEyQm9ziRrL0A4EEO7t3b7s3LmzBR0136ZcE0U2+zPL55cCuIa3gCxcLW5wovc452ehM9PirX9ddyqg1NNayrtcqVqu1BcA7r0OSCG4f8hxvmSOs4KA29O11bQ377HMSKkTRHRCEW0iKTcpovcBzNMyWVipdbiTue51kvICgPPm5F/GDID/AISQbRffDZUGAAAAAElFTkSuQmCC)](https://github.com/yujincheng08/BiliRoaming/releases/latest)\n[![Stars](https://img.shields.io/github/stars/yujincheng08/biliroaming?label=Stars&color=important&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAdCAYAAAC5UQwxAAADKUlEQVRIib2WX0iTURTAz737tpWEissM6c9cOUUkCqmHpYEtfAiFyl4yCoQefOmhHozAECGweskMw+hRitRQsD8ULSl0H1mUIDRr5myVpgvJQvP7f+P7Nsfc9s3dMTpw4ePee87vnHvvOeeD/y2IEEKNFOYHMom8lGHedGKWVjcloDJu7QLxRz7exTtpdRlqmurlot+KAHAqutRKsu/YeaQABgIge/e30upTR2hY6K8FEzhADfE3q9DqU0Uo+uoaQFCpQU01UmXS2UJjg+7RjCI3EHBoQFUIABFhGO0lFcmaSDpC6cuZ01p0kZcQilL21TQmayfpCMkoGkIA5TEuKlqkLL/dVWG2ONe80xggH7iXj4XPdiz5rUicKgDBZ8OC36Y+EsDggGj/1HlZ+2KJectXhSnwEaN1Ckw2n8zs8JrzTn1ftZ2bbjeb5i42gwHKkLy0QVNWwBE2hiNGIlEixopTGFjtvg0Zf4kEb+W8C1e1CCVP2XXm1/t9kAGO1NI5gajwJWBJVqEXlXrrNfNMybtzYu6RXuCBTTMOgAOW5FYOqjCIfKVGe3+baDnaC8tphC4Dq+Q4Xcg+eGllatUBGgv72kRLbXdaoBrskAvbXc2R0zE3Zix80C5Zjgeh9I0kAlb1DNufN0cv6eahOFnXYFzoPgmMUk4FE9Gwkl39EO8cuBZvOWHiK2NZj7H053C4lK0lMgDBxpdot1CptzNhEmCymKnlYrKiWiNiwg6kC+R/9uWAqGCqvEQASAIszHYWUwOx4CkNVxwaIeBAwoSdGogEb6wSClUOtWvwoe/oI1cbszBeqmdX97yR4C2KcYcL1kcpt/4O4PUcE7h1VqudplBJDDmAhU9F9EDxY3EYKGiFmZWzK11SXlOLOftgsA1t67gvT9Q0GhYeaUcJ5tDfgOS36tkFNS3iDWUUhsgbIOQ1uGXPnhtcoGej3l5u/sk6yeNoJSPgJiNAyDtwc/MvcLy98Q3MdJSQIXArY9YubqbTrgeKHnzgbr78oeQ2eQVu8VtTVbw9cRNfnL58APFzmxnbzR7do0kg4lRjNWGwZNp65Wkq+ukTAPgHIIGzcZjmG+EAAAAASUVORK5CYII=\n)](https://github.com/yujincheng08/BiliRoaming)\n\n# 哔哩漫游\n解除B站客户端番剧区域限制的Xposed模块，并且提供其他小功能\n\n# BiliRoaming\nAn Xposed module that unblocks bangumi area limit of BILIBILI, and miscellaneous features\n\n</div>\n\n# 支持以下功能\n\n- 解除B站番剧区域限制\n- 港澳台CDN加速\n- 缓存番剧\n- 支持国际版和概念版\n- 自定义主题色\n- 关闭青少年模式弹窗\n- 显示评论区楼层\n- ~概念版添加直播入口~\n- 不以小程序形式分享\n- 自动点赞视频\n- 把我的页面移到侧边栏\n- ~替换音乐状态栏为原生样式~\n- 提取视频、直播封面\n- 自定义屏启动图\n\n# Features\n\n- Unlock bangumi area limit\n- CDN speedup\n- Download bangumi\n- Support International ver and blue ver\n- Customize theme\n- Close teenager dialog\n- Show comment floor\n- ~Add live entry to blue ver~\n- Share without mini programs\n- Like videos automatically\n- Move 'Mine page' to sidebar drawer\n- ~Change music notification style to Primitive~\n- Extra covers from videos and live rooms\n- Customized splash images\n\n# download/下载\nhttps://github.com/yujincheng08/BiliRoaming/releases/latest\n\nhttps://modules.lsposed.org/module/me.iacn.biliroaming\n\n# 使用方法\nhttps://github.com/yujincheng08/BiliRoaming/wiki#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95\n\n# 特别鸣谢\n- [原作者](https://github.com/iAcn/BiliRoaming)\n- [BiliPlus](https://www.biliplus.com/)\n- [Kghost](https://github.com/kghost/bilibili-area-limit)\n\n### 交流\nTelegram: [@biliroaming](https://t.me/biliroaming)\n### License\n[GNU General Public License, version 3](LICENSE)\n\n\n### 本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助\n[亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)\n![](https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png)\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/release\n/.cxx\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import com.google.protobuf.gradle.*\n\nplugins {\n    alias(libs.plugins.agp.app)\n    alias(libs.plugins.kotlin)\n    alias(libs.plugins.protobuf)\n    alias(libs.plugins.lsplugin.resopt)\n    alias(libs.plugins.lsplugin.jgit)\n    alias(libs.plugins.lsplugin.apksign)\n    alias(libs.plugins.lsplugin.apktransform)\n    alias(libs.plugins.lsplugin.cmaker)\n}\n\nval appVerCode = jgit.repo()?.commitCount(\"refs/remotes/origin/master\") ?: 0\nval appVerName: String by rootProject\n\napksign {\n    storeFileProperty = \"releaseStoreFile\"\n    storePasswordProperty = \"releaseStorePassword\"\n    keyAliasProperty = \"releaseKeyAlias\"\n    keyPasswordProperty = \"releaseKeyPassword\"\n}\n\napktransform {\n    copy {\n        when (it.buildType) {\n            \"release\" -> file(\"${it.name}/BiliRoaming_${appVerName}.apk\")\n            else -> null\n        }\n    }\n}\n\ncmaker {\n    default {\n        targets(\"biliroaming\")\n        abiFilters(\"armeabi-v7a\", \"arm64-v8a\", \"x86\")\n        arguments += arrayOf(\n            \"-DANDROID_STL=none\",\n            \"-DCMAKE_CXX_STANDARD=23\",\n            \"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON\",\n        )\n        cFlags += \"-flto\"\n        cppFlags += \"-flto\"\n    }\n\n    buildTypes {\n        arguments += \"-DDEBUG_SYMBOLS_PATH=${layout.buildDirectory.file(\"symbols/${it.name}\").get().asFile.absolutePath}\"\n    }\n}\n\nandroid {\n    namespace = \"me.iacn.biliroaming\"\n    compileSdk = 35\n    buildToolsVersion = \"35.0.0\"\n    ndkVersion = \"29.0.14206865\"\n\n    buildFeatures {\n        prefab = true\n        buildConfig = true\n    }\n\n    defaultConfig {\n        applicationId = \"me.iacn.biliroaming\"\n        minSdk = 24\n        targetSdk = 35  // Target Android U\n        versionCode = appVerCode\n        versionName = appVerName\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            proguardFiles(\"proguard-rules.pro\")\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility(JavaVersion.VERSION_11)\n        targetCompatibility(JavaVersion.VERSION_11)\n    }\n\n    kotlinOptions {\n        jvmTarget = \"11\"\n        freeCompilerArgs = listOf(\n            \"-Xno-param-assertions\",\n            \"-Xno-call-assertions\",\n            \"-Xno-receiver-assertions\",\n            \"-language-version=2.0\",\n        )\n    }\n\n    sourceSets {\n        named(\"main\") {\n            proto {\n                srcDir(\"src/main/proto\")\n                include(\"**/*.proto\")\n            }\n        }\n    }\n\n    packaging {\n        resources {\n            excludes += \"**\"\n        }\n    }\n\n    lint {\n        checkReleaseBuilds = false\n    }\n\n    dependenciesInfo {\n        includeInApk = false\n    }\n\n    androidResources {\n        additionalParameters += arrayOf(\"--allow-reserved-package-id\", \"--package-id\", \"0x23\")\n    }\n\n    externalNativeBuild {\n        cmake {\n            path(\"src/main/jni/CMakeLists.txt\")\n            version = \"4.1.0+\"\n        }\n    }\n}\n\nprotobuf {\n    protoc {\n        artifact = libs.protobuf.protoc.get().toString()\n    }\n\n    generateProtoTasks {\n        all().forEach { task ->\n            task.builtins {\n                id(\"java\") {\n                    option(\"lite\")\n                }\n                id(\"kotlin\") {\n                    option(\"lite\")\n                }\n            }\n        }\n    }\n}\n\nconfigurations.all {\n    exclude(\"org.jetbrains.kotlin\", \"kotlin-stdlib-jdk7\")\n    exclude(\"org.jetbrains.kotlin\", \"kotlin-stdlib-jdk8\")\n}\n\ndependencies {\n    compileOnly(libs.xposed)\n    implementation(libs.protobuf.kotlin)\n    implementation(libs.protobuf.java)\n    compileOnly(libs.protobuf.protoc)\n    implementation(libs.kotlin.stdlib)\n    implementation(libs.kotlin.coroutines.android)\n    implementation(libs.kotlin.coroutines.jdk)\n    implementation(libs.androidx.documentfile)\n    implementation(libs.cxx)\n}\n\nval adbExecutable: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath\n\nval restartBiliBili = task(\"restartBiliBili\").apply {\n    doLast {\n        exec {\n            commandLine(adbExecutable, \"shell\", \"am\", \"force-stop\", \"tv.danmaku.bili\")\n        }\n        exec {\n            commandLine(\n                adbExecutable,\n                \"shell\",\n                \"am\",\n                \"start\",\n                \"$(pm resolve-activity --components tv.danmaku.bili)\"\n            )\n        }\n    }\n}\n\nafterEvaluate {\n    tasks.getByPath(\"installDebug\").finalizedBy(restartBiliBili)\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "-repackageclasses \"biliroaming\"\n\n-keep class me.iacn.biliroaming.XposedInit {\n    <init>();\n}\n\n-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {\n  <fields>;\n}\n\n-keepclasseswithmembers class me.iacn.biliroaming.utils.DexHelper {\n native <methods>;\n long token;\n java.lang.ClassLoader classLoader;\n}\n\n-keepattributes RuntimeVisible*Annotations\n\n-keepclassmembers class * {\n    @android.webkit.JavascriptInterface <methods>;\n}\n\n-keepclassmembers class * implements android.os.Parcelable {\n    public static final ** CREATOR;\n}\n\n-keepclassmembers class me.iacn.biliroaming.MainActivity$Companion {\n    boolean isModuleActive();\n}\n\n-assumenosideeffects class kotlin.jvm.internal.Intrinsics {\n    public static void check*(...);\n    public static void throw*(...);\n}\n\n-assumenosideeffects class java.util.Objects {\n    public static ** requireNonNull(...);\n}\n\n-allowaccessmodification\n-overloadaggressively\n"
  },
  {
    "path": "app/src/.gitignore",
    "content": "generated\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.MAIN\" />\n        </intent>\n    </queries>\n\n    <application\n        android:allowBackup=\"true\"\n        android:fullBackupContent=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/MainTheme\"\n        tools:ignore=\"GoogleAppIndexingWarning\">\n\n        <activity-alias\n            android:name=\".MainActivityAlias\"\n            android:exported=\"true\"\n            android:targetActivity=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity-alias>\n\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:showForAllUsers=\"true\"\n            android:launchMode=\"singleInstance\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"de.robv.android.xposed.category.MODULE_SETTINGS\" />\n            </intent-filter>\n        </activity>\n\n        <meta-data\n            android:name=\"xposedmodule\"\n            android:value=\"true\" />\n        <meta-data\n            android:name=\"xposeddescription\"\n            android:value=\"@string/xposed_description\" />\n        <meta-data\n            android:name=\"xposedminversion\"\n            android:value=\"53\" />\n\n        <meta-data\n            android:name=\"xposedscope\"\n            android:resource=\"@array/xposed_scope\" />\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/assets/xhook.js",
    "content": "(function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H=[].indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(b in this&&this[b]===a)return b;return-1};q=null,q=\"undefined\"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:\"undefined\"!=typeof global?global:a,x=q.document,d=\"before\",c=\"after\",o=\"readyState\",n=\"addEventListener\",m=\"removeEventListener\",h=\"dispatchEvent\",u=\"XMLHttpRequest\",g=\"fetch\",i=\"FormData\",p=[\"load\",\"loadend\",\"loadstart\"],e=[\"progress\",\"abort\",\"error\",\"timeout\"],E=\"undefined\"!=typeof navigator&&navigator.useragent?navigator.userAgent:\"\",A=parseInt((/msie (\\d+)/.exec(E.toLowerCase())||[])[1]),isNaN(A)&&(A=parseInt((/trident\\/.*; rv:(\\d+)/.exec(E.toLowerCase())||[])[1])),(G=Array.prototype).indexOf||(G.indexOf=function(a){var b,c,d,e;for(b=d=0,e=this.length;d<e;b=++d)if(c=this[b],c===a)return b;return-1}),D=function(a,b){return Array.prototype.slice.call(a,b)},w=function(a){return\"returnValue\"===a||\"totalSize\"===a||\"position\"===a},z=function(a,b){var c;for(c in a)if(a[c],!w(c))try{b[c]=a[c]}catch(a){}return b},B=function(a){return void 0===a?null:a},C=function(a,b,c){var d,e,f,g;for(e=function(a){return function(d){var e,f,g;e={};for(f in d)w(f)||(g=d[f],e[f]=g===b?c:g);return c[h](a,e)}},f=0,g=a.length;f<g;f++)d=a[f],c._has(d)&&(b[\"on\"+d]=e(d))},y=function(a){var b;if(x&&null!=x.createEventObject)return b=x.createEventObject(),b.type=a,b;try{return new Event(a)}catch(b){return{type:a}}},f=function(a){var c,d,e;return d={},e=function(a){return d[a]||[]},c={},c[n]=function(a,c,f){d[a]=e(a),d[a].indexOf(c)>=0||(f=f===b?d[a].length:f,d[a].splice(f,0,c))},c[m]=function(a,c){var f;if(a===b)return void(d={});c===b&&(d[a]=[]),f=e(a).indexOf(c),f!==-1&&e(a).splice(f,1)},c[h]=function(){var b,d,f,g,h,i,j,k;for(b=D(arguments),d=b.shift(),a||(b[0]=z(b[0],y(d))),g=c[\"on\"+d],g&&g.apply(c,b),k=e(d).concat(e(\"*\")),f=i=0,j=k.length;i<j;f=++i)h=k[f],h.apply(c,b)},c._has=function(a){return!(!d[a]&&!c[\"on\"+a])},a&&(c.listeners=function(a){return D(e(a))},c.on=c[n],c.off=c[m],c.fire=c[h],c.once=function(a,b){var d;return d=function(){return c.off(a,d),b.apply(null,arguments)},c.on(a,d)},c.destroy=function(){return d={}}),c},F=f(!0),F.EventEmitter=f,F[d]=function(a,b){if(a.length<1||a.length>2)throw\"invalid hook\";return F[n](d,a,b)},F[c]=function(a,b){if(a.length<2||a.length>3)throw\"invalid hook\";return F[n](c,a,b)},F.enable=function(){q[u]=t,\"function\"==typeof r&&(q[g]=r),k&&(q[i]=s)},F.disable=function(){q[u]=F[u],q[g]=F[g],k&&(q[i]=k)},v=F.headers=function(a,b){var c,d,e,f,g,h,i,j,k;switch(null==b&&(b={}),typeof a){case\"object\":d=[];for(e in a)g=a[e],f=e.toLowerCase(),d.push(f+\":\\t\"+g);return d.join(\"\\n\")+\"\\n\";case\"string\":for(d=a.split(\"\\n\"),i=0,j=d.length;i<j;i++)c=d[i],/([^:]+):\\s*(.+)/.test(c)&&(f=null!=(k=RegExp.$1)?k.toLowerCase():void 0,h=RegExp.$2,null==b[f]&&(b[f]=h));return b}},k=q[i],s=function(a){var b;this.fd=a?new k(a):new k,this.form=a,b=[],Object.defineProperty(this,\"entries\",{get:function(){var c;return c=a?D(a.querySelectorAll(\"input,select\")).filter(function(a){var b;return\"checkbox\"!==(b=a.type)&&\"radio\"!==b||a.checked}).map(function(a){return[a.name,\"file\"===a.type?a.files:a.value]}):[],c.concat(b)}}),this.append=function(a){return function(){var c;return c=D(arguments),b.push(c),a.fd.append.apply(a.fd,c)}}(this)},k&&(F[i]=k,q[i]=s),l=q[u],F[u]=l,t=q[u]=function(){var a,b,g,i,j,k,l,m,q,r,t,w,x,y,D,E,G,I,J,K,L;a=-1,I=new F[u],t={},y=null,l=void 0,D=void 0,w=void 0,r=function(){var b,c,d,e;if(w.status=y||I.status,y===a&&A<10||(w.statusText=I.statusText),y!==a){e=v(I.getAllResponseHeaders());for(b in e)d=e[b],w.headers[b]||(c=b.toLowerCase(),w.headers[c]=d)}},q=function(){if(I.responseType&&\"text\"!==I.responseType)\"document\"===I.responseType?(w.xml=I.responseXML,w.data=I.responseXML):w.data=I.response;else{w.text=I.responseText,w.data=I.responseText;try{w.xml=I.responseXML}catch(a){}}\"responseURL\"in I&&(w.finalUrl=I.responseURL)},G=function(){k.status=w.status,k.statusText=w.statusText},E=function(){\"text\"in w&&(k.responseText=w.text),\"xml\"in w&&(k.responseXML=w.xml),\"data\"in w&&(k.response=w.data),\"finalUrl\"in w&&(k.responseURL=w.finalUrl)},i=function(a){for(;a>b&&b<4;)k[o]=++b,1===b&&k[h](\"loadstart\",{}),2===b&&G(),4===b&&(G(),E()),k[h](\"readystatechange\",{}),4===b&&(t.async===!1?g():setTimeout(g,0))},g=function(){l||k[h](\"load\",{}),k[h](\"loadend\",{}),l&&(k[o]=0)},b=0,x=function(a){var b,d;if(4!==a)return void i(a);b=F.listeners(c),(d=function(){var a;return b.length?(a=b.shift(),2===a.length?(a(t,w),d()):3===a.length&&t.async?a(t,w,d):d()):i(4)})()},k=t.xhr=f(),I.onreadystatechange=function(a){try{2===I[o]&&r()}catch(a){}4===I[o]&&(D=!1,r(),q()),x(I[o])},m=function(){l=!0},k[n](\"error\",m),k[n](\"timeout\",m),k[n](\"abort\",m),k[n](\"progress\",function(){b<3?x(3):k[h](\"readystatechange\",{})}),(\"withCredentials\"in I||F.addWithCredentials)&&(k.withCredentials=!1),k.status=0,L=e.concat(p);for(J=0,K=L.length;J<K;J++)j=L[J],k[\"on\"+j]=null;return k.open=function(a,c,d,e,f){b=0,l=!1,D=!1,t.headers={},t.headerNames={},t.status=0,w={},w.headers={},t.method=a,t.url=c,t.async=d!==!1,t.user=e,t.pass=f,x(1)},k.send=function(a){var b,c,f,g,h,i,j,l;for(l=[\"type\",\"timeout\",\"withCredentials\"],i=0,j=l.length;i<j;i++)c=l[i],f=\"type\"===c?\"responseType\":c,f in k&&(t[c]=k[f]);t.body=a,h=function(){var a,b,d,g,h,i;for(C(e,I,k),k.upload&&C(e.concat(p),I.upload,k.upload),D=!0,I.open(t.method,t.url,t.async,t.user,t.pass),h=[\"type\",\"timeout\",\"withCredentials\"],d=0,g=h.length;d<g;d++)c=h[d],f=\"type\"===c?\"responseType\":c,c in t&&(I[f]=t[c]);i=t.headers;for(a in i)b=i[a],a&&I.setRequestHeader(a,b);t.body instanceof s&&(t.body=t.body.fd),I.send(t.body)},b=F.listeners(d),(g=function(){var a,c;return b.length?(a=function(a){if(\"object\"==typeof a&&(\"number\"==typeof a.status||\"number\"==typeof w.status))return z(a,w),H.call(a,\"data\")<0&&(a.data=a.response||a.text),void x(4);g()},a.head=function(a){return z(a,w),x(2)},a.progress=function(a){return z(a,w),x(3)},c=b.shift(),1===c.length?a(c(t)):2===c.length&&t.async?c(t,a):a()):h()})()},k.abort=function(){y=a,D?I.abort():k[h](\"abort\",{})},k.setRequestHeader=function(a,b){var c,d;c=null!=a?a.toLowerCase():void 0,d=t.headerNames[c]=t.headerNames[c]||a,t.headers[d]&&(b=t.headers[d]+\", \"+b),t.headers[d]=b},k.getResponseHeader=function(a){var b;return b=null!=a?a.toLowerCase():void 0,B(w.headers[b])},k.getAllResponseHeaders=function(){return B(v(w.headers))},I.overrideMimeType&&(k.overrideMimeType=function(){return I.overrideMimeType.apply(I,arguments)}),I.upload&&(k.upload=t.upload=f()),k.UNSENT=0,k.OPENED=1,k.HEADERS_RECEIVED=2,k.LOADING=3,k.DONE=4,k.response=\"\",k.responseText=\"\",k.responseXML=null,k.readyState=0,k.statusText=\"\",k},\"function\"==typeof q[g]&&(j=q[g],F[g]=j,r=q[g]=function(a,b){var e,f,g;return null==b&&(b={headers:{}}),b.url=a,g=null,f=F.listeners(d),e=F.listeners(c),new Promise(function(a,c){var d,h,i,k,l;h=function(){return b.body instanceof s&&(b.body=b.body.fd),b.headers&&(b.headers=new Headers(b.headers)),g||(g=new Request(b.url,b)),z(b,g)},i=function(b){var c;return e.length?(c=e.shift(),2===c.length?(c(h(),b),i(b)):3===c.length?c(h(),b,i):i(b)):a(b)},d=function(b){var c;if(void 0!==b)return c=new Response(b.body||b.text,b),a(c),void i(c);k()},k=function(){var a;return f.length?(a=f.shift(),1===a.length?d(a(b)):2===a.length?a(h(),d):void 0):void l()},l=function(){return j(h()).then(function(a){return i(a)}).catch(function(a){return i(a),c(a)})},k()})}),t.UNSENT=0,t.OPENED=1,t.HEADERS_RECEIVED=2,t.LOADING=3,t.DONE=4,\"function\"==typeof define&&define.amd?define(\"xhook\",[],function(){return F}):\"object\"==typeof module&&module.exports?module.exports={xhook:F}:q&&(q.xhook=F)}).call(this,window);\n\nwindow.XMLHttpRequest.prototype.addEventListener = xhook.addEventListener;\n\nxhook.after(function(request, response) {\n    response.text = hooker.hook(request.url, response.text)\n});\n"
  },
  {
    "path": "app/src/main/assets/xposed_init",
    "content": "me.iacn.biliroaming.XposedInit\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/ARGBColorChooseDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.AlertDialog\nimport android.content.Context\nimport android.graphics.Color\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.view.View\nimport android.widget.EditText\nimport android.widget.SeekBar\nimport android.widget.SeekBar.OnSeekBarChangeListener\nimport android.widget.TextView\nimport me.iacn.biliroaming.utils.inflateLayout\n\n/**\n * Created by iAcn on 2019/7/14\n * Email i@iacn.me\n *\n * Copy & Modify from ColorChooseDialog on 2021/7/6\n */\nclass ARGBColorChooseDialog(context: Context, defColor: Int) : AlertDialog.Builder(context) {\n    private val view = context.inflateLayout(R.layout.dialog_argb_color_choose)\n    private val sampleView: View = view.findViewById(R.id.view_sample2)\n    private val etColor: EditText = view.findViewById(R.id.et_color2)\n    private val sbColorA: SeekBar = view.findViewById(R.id.sb_colorA2)\n    private val sbColorR: SeekBar = view.findViewById(R.id.sb_colorR2)\n    private val sbColorG: SeekBar = view.findViewById(R.id.sb_colorG2)\n    private val sbColorB: SeekBar = view.findViewById(R.id.sb_colorB2)\n    private val tvColorA: TextView = view.findViewById(R.id.tv_colorA2)\n    private val tvColorR: TextView = view.findViewById(R.id.tv_colorR2)\n    private val tvColorG: TextView = view.findViewById(R.id.tv_colorG2)\n    private val tvColorB: TextView = view.findViewById(R.id.tv_colorB2)\n    val color: Int\n        get() = Color.argb(\n            sbColorA.progress,\n            sbColorR.progress,\n            sbColorG.progress,\n            sbColorB.progress\n        )\n\n    private fun setEditTextListener() {\n        etColor.addTextChangedListener(object : TextWatcher {\n            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}\n            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {\n                updateValue(handleUnknownColor(s.toString()))\n            }\n\n            override fun afterTextChanged(s: Editable) {}\n        })\n    }\n\n    private fun setSeekBarListener() {\n        val listener: OnSeekBarChangeListener = object : OnSeekBarChangeListener {\n            override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {\n                if (fromUser) {\n                    val color = Color.argb(\n                        sbColorA.progress,\n                        sbColorR.progress,\n                        sbColorG.progress,\n                        sbColorB.progress\n                    )\n                    etColor.setText(String.format(\"%08X\", 0xFFFFFFFF.toInt() and color))\n                }\n                tvColorA.text = sbColorA.progress.toString()\n                tvColorR.text = sbColorR.progress.toString()\n                tvColorG.text = sbColorG.progress.toString()\n                tvColorB.text = sbColorB.progress.toString()\n            }\n\n            override fun onStartTrackingTouch(seekBar: SeekBar) {}\n            override fun onStopTrackingTouch(seekBar: SeekBar) {}\n        }\n        sbColorA.setOnSeekBarChangeListener(listener)\n        sbColorR.setOnSeekBarChangeListener(listener)\n        sbColorG.setOnSeekBarChangeListener(listener)\n        sbColorB.setOnSeekBarChangeListener(listener)\n    }\n\n    private fun updateValue(color: Int) {\n        sampleView.setBackgroundColor(color)\n        val progressA = Color.alpha(color)\n        val progressR = Color.red(color)\n        val progressG = Color.green(color)\n        val progressB = Color.blue(color)\n        sbColorA.progress = progressA\n        sbColorR.progress = progressR\n        sbColorG.progress = progressG\n        sbColorB.progress = progressB\n        tvColorA.text = progressA.toString()\n        tvColorR.text = progressR.toString()\n        tvColorG.text = progressG.toString()\n        tvColorB.text = progressB.toString()\n    }\n\n    private fun handleUnknownColor(color: String) =\n        try {\n            Color.parseColor(\"#$color\")\n        } catch (e: IllegalArgumentException) {\n            Color.BLACK\n        }\n\n    init {\n        setView(view)\n        setEditTextListener()\n        setSeekBarListener()\n        updateValue(defColor)\n        etColor.setText(String.format(\"%08X\", 0xFFFFFFFF.toInt() and defColor))\n        setTitle(\"拾色器\")\n        setNegativeButton(\"取消\", null)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/BaseWidgetDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.AlertDialog\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.Typeface\nimport android.text.TextUtils\nimport android.util.TypedValue\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.inputmethod.EditorInfo\nimport android.widget.*\nimport me.iacn.biliroaming.utils.children\nimport me.iacn.biliroaming.utils.dp\nimport me.iacn.biliroaming.utils.setRippleBackground\n\nopen class BaseWidgetDialog(context: Context) : AlertDialog.Builder(context) {\n    protected fun string(resId: Int) = context.getString(resId)\n\n    protected fun categoryTitle(title: String) = TextView(context).apply {\n        text = title\n        typeface = Typeface.DEFAULT_BOLD\n        setTextSize(TypedValue.COMPLEX_UNIT_SP, 18F)\n        layoutParams = LinearLayout.LayoutParams(\n            LinearLayout.LayoutParams.MATCH_PARENT,\n            LinearLayout.LayoutParams.WRAP_CONTENT\n        ).apply { topMargin = 10.dp }\n    }\n\n    protected fun textInputTitle(title: String) = TextView(context).apply {\n        text = title\n        setTextSize(TypedValue.COMPLEX_UNIT_SP, 18F)\n        layoutParams = LinearLayout.LayoutParams(\n            LinearLayout.LayoutParams.WRAP_CONTENT,\n            LinearLayout.LayoutParams.WRAP_CONTENT\n        )\n    }\n\n    protected fun textInputItem(\n        name: String,\n        type: Int = EditorInfo.TYPE_CLASS_NUMBER\n    ): Pair<LinearLayout, EditText> {\n        val layout = LinearLayout(context).apply {\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        val textView = TextView(context).apply {\n            text = name\n            setSingleLine()\n            ellipsize = TextUtils.TruncateAt.END\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        val editText = EditText(context).apply {\n            setSingleLine()\n            inputType = type\n            layoutParams = LinearLayout.LayoutParams(\n                0,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            ).apply { weight = 1F }\n        }\n        layout.addView(textView)\n        layout.addView(editText)\n        return Pair(layout, editText)\n    }\n\n    private fun keywordTypeHeader(\n        group: ViewGroup,\n        name: String,\n        namMinWidth: Int,\n        showRegex: Boolean,\n        onAdd: (v: View) -> Unit,\n    ): Triple<LinearLayout, Button, Switch> {\n        val layout = LinearLayout(context).apply {\n            gravity = Gravity.START\n            orientation = LinearLayout.HORIZONTAL\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        val nameView = TextView(context).apply {\n            text = name\n            typeface = Typeface.DEFAULT_BOLD\n            setTextSize(TypedValue.COMPLEX_UNIT_SP, 16F)\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n            minWidth = namMinWidth\n        }\n        val addView = Button(context).apply {\n            text = string(R.string.keyword_group_add)\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n            minWidth = 60.dp\n            minimumWidth = 60.dp\n            setOnClickListener(onAdd)\n        }\n        val clearView = Button(context).apply {\n            text = string(R.string.keyword_group_clear)\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n            visibility = View.GONE\n            minWidth = 60.dp\n            minimumWidth = 60.dp\n            setOnClickListener { group.removeAllViews() }\n        }\n        val regexModeView = TextView(context).apply {\n            text = string(R.string.regex_mode)\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            ).apply { marginStart = 4.dp }\n        }\n        val regexModeSwitch = Switch(context).apply {\n            isSoundEffectsEnabled = false\n            isHapticFeedbackEnabled = false\n        }\n        if (!showRegex) {\n            regexModeView.visibility = View.GONE\n            regexModeSwitch.visibility = View.GONE\n        }\n        layout.addView(nameView)\n        layout.addView(addView)\n        layout.addView(clearView)\n        layout.addView(regexModeView)\n        layout.addView(regexModeSwitch)\n        return Triple(layout, clearView, regexModeSwitch)\n    }\n\n    protected fun keywordInputItem(\n        parent: ViewGroup,\n        keyword: String = \"\",\n        type: Int = EditorInfo.TYPE_CLASS_TEXT,\n    ): Pair<LinearLayout, EditText> {\n        val layout = LinearLayout(context).apply {\n            orientation = LinearLayout.HORIZONTAL\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        val editText = EditText(context).apply {\n            inputType = type\n            setText(keyword)\n            setTextSize(TypedValue.COMPLEX_UNIT_SP, 16F)\n            setSingleLine()\n            layoutParams = LinearLayout.LayoutParams(\n                0,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            ).apply { weight = 1F }\n        }\n        val button = Button(context).apply {\n            text = string(R.string.keyword_group_delete)\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n            minWidth = 60.dp\n            minimumWidth = 60.dp\n            setOnClickListener { parent.removeView(layout) }\n        }\n        layout.addView(editText)\n        layout.addView(button)\n        return Pair(layout, editText)\n    }\n\n    protected fun LinearLayout.addKeywordGroup(\n        name: String,\n        namMinWidth: Int = 64.dp,\n        showRegex: Boolean = false,\n        inputType: Int = EditorInfo.TYPE_CLASS_TEXT,\n    ): Pair<ViewGroup, Switch> {\n        val group = LinearLayout(context).apply {\n            orientation = LinearLayout.VERTICAL\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        val (_, clearButton, regexModeSwitch) = keywordTypeHeader(\n            group, name, namMinWidth, showRegex\n        ) {\n            keywordInputItem(group, type = inputType).let {\n                group.addView(it.first)\n                it.second.requestFocus()\n            }\n        }.also { addView(it.first) }\n        group.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {\n            override fun onChildViewAdded(parent: View, child: View) {\n                if (group.childCount == 0) {\n                    clearButton.visibility = View.GONE\n                } else {\n                    clearButton.visibility = View.VISIBLE\n                }\n            }\n\n            override fun onChildViewRemoved(parent: View, child: View) {\n                if (group.childCount == 0) {\n                    clearButton.visibility = View.GONE\n                } else {\n                    clearButton.visibility = View.VISIBLE\n                }\n            }\n        })\n        addView(group)\n        return Pair(group, regexModeSwitch)\n    }\n\n    protected fun switchPrefsItem(title: String): Pair<LinearLayout, Switch> {\n        val layout = LinearLayout(context).apply {\n            gravity = Gravity.CENTER_VERTICAL\n            orientation = LinearLayout.HORIZONTAL\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        val titleView = TextView(context).apply {\n            text = title\n            setTextSize(TypedValue.COMPLEX_UNIT_SP, 16F)\n            setSingleLine()\n            ellipsize = TextUtils.TruncateAt.END\n            setPadding(0, 8.dp, 0, 8.dp)\n            layoutParams = LinearLayout.LayoutParams(\n                0,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            ).apply { weight = 1F; marginEnd = 10.dp }\n        }\n        val switcher = Switch(context).apply {\n            isClickable = false\n            isSoundEffectsEnabled = false\n            isHapticFeedbackEnabled = false\n            setBackgroundColor(Color.TRANSPARENT)\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.WRAP_CONTENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        layout.setOnClickListener { switcher.toggle() }\n        layout.setRippleBackground()\n        layout.addView(titleView)\n        layout.addView(switcher)\n        return Pair(layout, switcher)\n    }\n\n    protected fun ViewGroup.getKeywords() = children\n        .filterIsInstance<ViewGroup>()\n        .flatMap { it.children }\n        .filterIsInstance<EditText>()\n        .map { it.text.toString().trim() }\n        .filter { it.isNotEmpty() }\n        .toSet()\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming\n\nimport android.app.AndroidAppHelper\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.net.Uri\nimport android.text.style.ClickableSpan\nimport android.text.style.LineBackgroundSpan\nimport android.util.SparseArray\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport dalvik.system.BaseDexClassLoader\nimport me.iacn.biliroaming.utils.*\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.io.InputStream\nimport java.lang.reflect.Constructor\nimport java.lang.reflect.Method\nimport java.lang.reflect.Modifier\nimport java.lang.reflect.ParameterizedType\nimport java.util.Objects\nimport kotlin.math.max\nimport kotlin.system.measureTimeMillis\nimport kotlin.time.ExperimentalTime\nimport kotlin.time.measureTimedValue\n\n\ninfix fun Configs.Class.from(cl: ClassLoader) = if (hasName()) name.findClassOrNull(cl) else null\nval Configs.Method.orNull get() = if (hasName()) name else null\nval Configs.Field.orNull get() = if (hasName()) name else null\n\nclass BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContext: Context) {\n    init {\n        instance = this\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private val mHookInfo: Configs.HookInfo = run {\n        val (result, time) = measureTimedValue { readHookInfo(mContext) }\n        Log.d(\"load hookinfo $time\")\n        Log.d(result.copy { clearMapIds() }.print())\n        result\n    }\n    val bangumiApiResponseClass by Weak { mHookInfo.bangumiApiResponse from mClassLoader }\n    val rxGeneralResponseClass by Weak { \"com.bilibili.okretro.call.rxjava.RxGeneralResponse\" from mClassLoader }\n    val fastJsonClass by Weak { mHookInfo.fastJson.class_ from mClassLoader }\n    val bangumiUniformSeasonClass by Weak { mHookInfo.bangumiSeason from mClassLoader }\n    val kingPositionComponentClass by Weak { mHookInfo.autoLike.kingPositionComponent.class_ from mClassLoader }\n    val storyAbsControllerClass by Weak { mHookInfo.autoLike.storyAbsController.class_ from mClassLoader }\n    val storyDetailClass by Weak { \"com.bilibili.video.story.StoryDetail\" from mClassLoader }\n    val retrofitResponseClass by Weak { mHookInfo.retrofitResponse from mClassLoader }\n    val themeHelperClass by Weak { mHookInfo.themeHelper.class_ from mClassLoader }\n    val themeIdHelperClass by Weak { mHookInfo.themeIdHelper.class_ from mClassLoader }\n    val columnHelperClass by Weak { mHookInfo.columnHelper.class_ from mClassLoader }\n    val settingRouterClass by Weak { mHookInfo.settings.settingRouter from mClassLoader }\n    val themeListClickClass by Weak { mHookInfo.themeListClick from mClassLoader }\n    val themeNameClass by Weak { mHookInfo.themeName.class_ from mClassLoader }\n    val themeProcessorClass by Weak { mHookInfo.themeProcessor.class_ from mClassLoader }\n    val drawerClass by Weak { mHookInfo.drawer.class_ from mClassLoader }\n    val generalResponseClass by Weak { mHookInfo.generalResponse from mClassLoader }\n    val seasonParamsMapClass by Weak { \"com.bilibili.bangumi.data.page.detail.BangumiDetailApiService\\$UniformSeasonParamsMap\" from mClassLoader }\n    val seasonParamsClass by Weak { mHookInfo.bangumiParams.class_ from mClassLoader }\n    val brandSplashClass by Weak { \"tv.danmaku.bili.ui.splash.brand.ui.BaseBrandSplashFragment\" from mClassLoader }\n    val urlConnectionClass by Weak { \"com.bilibili.lib.okhttp.huc.OkHttpURLConnection\" from mClassLoader }\n    val downloadThreadListenerClass by Weak { mHookInfo.downloadThread.listener from mClassLoader }\n    val downloadThreadViewHostClass by Weak { mHookInfo.downloadThread.viewHost from mClassLoader }\n    val reportDownloadThreadClass by Weak { mHookInfo.downloadThread.reportDownload.class_ from mClassLoader }\n    val libBiliClass by Weak { mHookInfo.signQuery.class_ from mClassLoader }\n    val splashActivityClass by Weak {\n        \"tv.danmaku.bili.ui.splash.SplashActivity\" from mClassLoader\n            ?: \"tv.danmaku.bili.MainActivityV2\" from mClassLoader\n    }\n    val mainActivityClass by Weak { \"tv.danmaku.bili.MainActivityV2\" from mClassLoader }\n    val gripperBootExpClass by Weak { \"com.bilibili.gripper.exp.a\\$a\" from mClassLoader }\n    val homeUserCenterClass by Weak { if (mHookInfo.settings.homeUserCenterCount == 1) mHookInfo.settings.homeUserCenterList.first().class_ from mClassLoader else null }\n    val menuGroupItemClass by Weak { mHookInfo.settings.menuGroupItem from mClassLoader }\n    val drawerLayoutClass by Weak { mHookInfo.drawer.layout from mClassLoader }\n    val drawerLayoutParamsClass by Weak { mHookInfo.drawer.layoutParams from mClassLoader }\n    val splashInfoClass by Weak {\n        \"tv.danmaku.bili.ui.splash.brand.BrandShowInfo\" from mClassLoader\n            ?: \"tv.danmaku.bili.ui.splash.brand.model.BrandShowInfo\" from mClassLoader\n    }\n    val commentCopyClass by Weak { mHookInfo.commentLongClick from mClassLoader }\n    val commentCopyNewClass by Weak { mHookInfo.commentLongClickNew from mClassLoader }\n    val comment3CopyClass by Weak { mHookInfo.comment3Copy.class_ from mClassLoader }\n    val kotlinJsonClass by Weak { \"kotlinx.serialization.json.Json\" from mClassLoader }\n    val gsonConverterClass by Weak { mHookInfo.gsonHelper.gsonConverter from mClassLoader }\n    val playerCoreServiceV2Class by Weak { mHookInfo.playerCoreService.class_ from mClassLoader }\n    val teenagersModeDialogActivityClass by Weak {\n        \"com.bilibili.teenagersmode.ui.TeenagersModeDialogActivity\" from mClassLoader\n    }\n    val pegasusFeedClass by Weak { mHookInfo.pegasusFeed.class_ from mClassLoader }\n    val popularClass by Weak { mHookInfo.popular.class_ from mClassLoader }\n    val subtitleSpanClass by Weak { mHookInfo.subtitleSpan from mClassLoader }\n    val chronosSwitchClass by Weak { mHookInfo.chronosSwitch from mClassLoader }\n    val biliSpaceClass by Weak { \"com.bilibili.app.authorspace.api.BiliSpace\" from mClassLoader }\n    val biliVideoDetailClass by Weak {\n        (\"tv.danmaku.bili.ui.video.api.BiliVideoDetail\" from mClassLoader)\n            ?: (\"tv.danmaku.bili.videopage.data.view.model.BiliVideoDetail\" from mClassLoader)\n    }\n    val commentSpanTextViewClass by Weak { mHookInfo.commentSpan from mClassLoader }\n    val commentSpanEllipsisTextViewClass by Weak { \"com.bilibili.app.comm.comment2.widget.CommentSpanEllipsisTextView\" from mClassLoader }\n    val liveRoomActivityClass by Weak { \"com.bilibili.bililive.room.ui.roomv3.LiveRoomActivityV3\" from mClassLoader }\n    val liveKvConfigHelperClass by Weak { \"com.bilibili.bililive.tec.kvcore.LiveKvConfigHelper\" from mClassLoader }\n    val storyVideoActivityClass by Weak { \"com.bilibili.video.story.StoryVideoActivity\" from mClassLoader }\n    val okioWrapperClass by Weak { mHookInfo.okio.class_ from mClassLoader }\n    val ellipsizingTextViewClass by Weak { \"com.bilibili.bplus.followingcard.widget.EllipsizingTextView\" from mClassLoader }\n    val shareClickResultClass by Weak { \"com.bilibili.lib.sharewrapper.online.api.ShareClickResult\" from mClassLoader }\n    val kanbanCallbackClass by Weak { mHookInfo.kanBan.class_ from mClassLoader }\n    val toastHelperClass by Weak { mHookInfo.toastHelper.class_ from mClassLoader }\n    val biliAccountsClass by Weak { mHookInfo.biliAccounts.class_ from mClassLoader }\n    val networkExceptionClass by Weak { \"com.bilibili.lib.moss.api.NetworkException\" from mClassLoader }\n    val brotliInputStreamClass by Weak { mHookInfo.brotliInputStream from mClassLoader }\n    val commentInvalidFragmentClass by Weak {\n        \"com.bilibili.bangumi.ui.page.detail.OGVCommentFragment\".from(mClassLoader)\n            ?: \"com.bilibili.bangumi.ui.page.detail.BangumiCommentInvalidFragmentV2\"\n                .from(mClassLoader)\n    }\n    val playerQualityServiceClass by Weak { \"com.bilibili.playerbizcommon.features.quality.PlayerQualityService\" from mClassLoader }\n    val mossResponseHandlerClass by Weak { \"com.bilibili.lib.moss.api.MossResponseHandler\" from mClassLoader }\n    val projectionPlayUrlClass by Weak { \"com.bilibili.lib.projection.internal.api.model.ProjectionPlayUrl\" from mClassLoader }\n    val responseBodyClass by Weak { mHookInfo.okHttp.responseBody.class_ from mClassLoader }\n    val mediaTypeClass by Weak { mHookInfo.okHttp.mediaType.class_ from mClassLoader }\n    val biliCallClass by Weak { mHookInfo.biliCall.class_ from mClassLoader }\n    val parserClass by Weak { mHookInfo.biliCall.parser from mClassLoader }\n    val livePagerRecyclerViewClass by Weak { mHookInfo.livePagerRecyclerView from mClassLoader }\n    val cronCanvasClass by Weak { \"com.bilibili.cron.Canvas\" from mClassLoader }\n    val subtitleConfigGetClass by Weak { \"tv.danmaku.biliplayerv2.service.interact.biz.chronos.chronosrpc.methods.receive.GetDanmakuConfig\\$SubtitleConfig\" from mClassLoader }\n    val subtitleConfigChangeClass by Weak { \"tv.danmaku.biliplayerv2.service.interact.biz.chronos.chronosrpc.methods.send.DanmakuConfigChange\\$SubtitleConfig\" from mClassLoader }\n    val liveRoomPlayerViewClass by Weak { \"com.bilibili.bililive.room.ui.roomv3.player.container.LiveRoomPlayerContainerView\" from mClassLoader }\n    val biliConfigClass by Weak { mHookInfo.biliConfig.class_ from mClassLoader }\n    val updateInfoSupplierClass by Weak { mHookInfo.updateInfoSupplier.class_ from mClassLoader }\n    val latestVersionExceptionClass by Weak { \"tv.danmaku.bili.update.internal.exception.LatestVersionException\" from mClassLoader }\n    val playerPreloadHolderClass by Weak { mHookInfo.playerPreloadHolder.class_ from mClassLoader }\n    val playerSettingHelperClass by Weak { mHookInfo.playerSettingHelper.class_ from mClassLoader }\n    val autoSupremumQualityClass by Weak { mHookInfo.autoSupremumQuality.class_ from mClassLoader }\n    val qualityStrategyProviderClass by Weak { mHookInfo.qualityStrategyProvider.class_ from mClassLoader }\n    val liveRtcEnableClass by Weak { mHookInfo.liveRtcHelper.liveRtcEnableClass from mClassLoader }\n    val playURLMossClass by Weak { \"com.bapis.bilibili.app.playurl.v1.PlayURLMoss\" from mClassLoader }\n    val playViewReqClass by Weak { \"com.bapis.bilibili.app.playurl.v1.PlayViewReq\" from mClassLoader }\n    val playerMossClass by Weak { \"com.bapis.bilibili.app.playerunite.v1.PlayerMoss\" from mClassLoader }\n    val playViewUniteReqClass by Weak { \"com.bapis.bilibili.app.playerunite.v1.PlayViewUniteReq\" from mClassLoader }\n    val viewMossClass by Weak { \"com.bapis.bilibili.app.view.v1.ViewMoss\" from mClassLoader }\n    val viewReqClass by Weak { \"com.bapis.bilibili.app.view.v1.ViewReq\" from mClassLoader }\n    val viewUniteMossClass by Weak { \"com.bapis.bilibili.app.viewunite.v1.ViewMoss\" from mClassLoader }\n    val viewUniteReqClass by Weak { \"com.bapis.bilibili.app.viewunite.v1.ViewReq\" from mClassLoader }\n    val bkArcPartClass by Weak { \"com.bapis.bilibili.app.listener.v1.BKArcPart\" from mClassLoader }\n    val builtInThemesClass by Weak { mHookInfo.builtInThemes.class_ from mClassLoader }\n    val themeColorsConstructor by Weak {\n        mHookInfo.themeColors.from(mClassLoader)?.declaredConstructors?.firstOrNull { it.isPrivate }\n            ?.apply { isAccessible = true }\n    }\n    val biliGlobalPreferenceClass by Weak { mHookInfo.biliGlobalPreference.class_ from mClassLoader }\n    val dmMossClass by Weak { \"com.bapis.bilibili.community.service.dm.v1.DMMoss\" from mClassLoader }\n    val dmViewReqClass by Weak { \"com.bapis.bilibili.community.service.dm.v1.DmViewReq\" from mClassLoader }\n    val treePointItemClass by Weak { \"com.bilibili.app.comm.list.common.data.ThreePointItem\" from mClassLoader }\n    val dislikeReasonClass by Weak { \"com.bilibili.app.comm.list.common.data.DislikeReason\" from mClassLoader }\n    val cardClickProcessorClass by Weak { mHookInfo.cardClickProcessor.class_ from mClassLoader }\n    val publishToFollowingConfigClass by Weak { mHookInfo.publishToFollowingConfig from mClassLoader }\n    val imageFragmentClass by Weak { \"com.bilibili.lib.imageviewer.fragment.ImageFragment\" from mClassLoader }\n    val unknownFieldSetLiteInstance by Weak {\n        \"com.google.protobuf.UnknownFieldSetLite\".from(mClassLoader)\n            ?.callStaticMethod(\"getDefaultInstance\")\n    }\n    val searchAllResponseClass by Weak { \"com.bapis.bilibili.polymer.app.search.v1.SearchAllResponse\" from mClassLoader }\n    val searchVideoCardClass by Weak { \"com.bapis.bilibili.polymer.app.search.v1.SearchVideoCard\" from mClassLoader }\n    val playSpeedManager by Weak { mHookInfo.playSpeedManager from mClassLoader }\n    val continuationClass by Weak { mHookInfo.continuation.class_ from mClassLoader }\n    val vipQualityTrialService by Weak { mHookInfo.vipQualityTrialService.class_ from mClassLoader }\n    val livePlayUrlSelectUtilClass by Weak { mHookInfo.liveQuality.selectUtil.class_ from mClassLoader }\n    val liveRTCSourceServiceImplClass by Weak { mHookInfo.liveQuality.sourceService.class_ from mClassLoader }\n    val defaultRequestInterceptClass by Weak { mHookInfo.liveQuality.interceptor.class_ from mClassLoader }\n    val httpUrlClass by Weak { mHookInfo.okHttp.httpUrl.class_ from mClassLoader }\n    val preBuiltConfigClass by Weak { mHookInfo.preBuiltConfig.class_ from mClassLoader }\n    val dataSPClass by Weak { mHookInfo.dataSP.class_ from mClassLoader }\n    val fastjsonFieldAnnotation by Weak { \"com.alibaba.fastjson.annotation.JSONField\" from mClassLoader }\n    val gsonFieldAnnotation by Weak { \"com.google.gson.annotations.SerializedName\" from mClassLoader }\n    val pegasusParserClass by Weak { mHookInfo.pegasusParser from mClassLoader }\n    val resolveClientCompanionClass by Weak { mHookInfo.resolveClientCompanion.class_ from mClassLoader }\n    val videoDownloadEntryClass by Weak { \"com.bilibili.videodownloader.model.VideoDownloadEntry\" from mClassLoader }\n    val rewardAdClass by Weak { mHookInfo.rewardAd.class_ from mClassLoader }\n    val tripleSpeedServiceClass by Weak { \"com.bilibili.ship.theseus.united.player.TripleSpeedService\\$runOldTripleSpeed\\$1\\$listener\\$1\\$onLongPress\\$1\" from mClassLoader }\n    val storyPagerPlayerClass by Weak { mHookInfo.storyPagerPlayer.class_ from mClassLoader }\n\n    // for v8.17.0+\n    val useNewMossFunc = instance.viewMossClass?.declaredMethods?.any {\n        it.name == \"executeRelatesFeed\"\n    } ?: false\n\n    private val unitedVideoActivity by Weak { \"com.bilibili.ship.theseus.detail.UnitedBizDetailsActivity\" from mClassLoader }\n    val hasUnitedVideoActivity = !Objects.isNull(unitedVideoActivity)\n\n    val ids: Map<String, Int> by lazy {\n        mHookInfo.mapIds.idsMap\n    }\n\n    fun getCustomizeAccessKey(area: String): String? {\n        var key = sPrefs.getString(\"${area}_accessKey\", null)\n        if (key.isNullOrBlank()) key = accessKey\n        return key\n    }\n\n    val biliAccounts by lazy {\n        biliAccountsClass?.callStaticMethodOrNull(\n            mHookInfo.biliAccounts.get.orNull,\n            AndroidAppHelper.currentApplication()\n        )\n    }\n\n    val accessKey by lazy {\n        biliAccounts?.callMethodOrNullAs<String>(mHookInfo.biliAccounts.getAccessKey.orNull)\n    }\n\n    val appKey by lazy {\n        mHookInfo.biliConfig.getAppKey.orNull?.let {\n            biliConfigClass?.callStaticMethodOrNullAs<String>(it)\n        } ?: appKeyMap[packageName] ?: \"1d8b6e7d45233436\"\n    }\n\n    val clientVersionCode get() = mHookInfo.clientVersionCode\n\n    fun fastJsonParse() = mHookInfo.fastJson.parse.orNull\n\n    fun colorArray() = mHookInfo.themeHelper.colorArray.orNull\n\n    fun colorId() = mHookInfo.themeIdHelper.colorId.orNull\n\n    fun columnColorArray() = mHookInfo.columnHelper.colorArray.orNull\n\n    fun signQueryName() = mHookInfo.signQuery.method.orNull\n\n    fun skinList() = mHookInfo.skinList.orNull\n\n    fun themeReset() = mHookInfo.themeProcessor.methodsList.map { it.orNull }\n\n    fun homeCenters() = mHookInfo.settings.homeUserCenterList.map {\n        it.class_ from mClassLoader to it.addSetting.orNull\n    }\n\n    fun requestField() = mHookInfo.okHttp.response.request.orNull\n\n    fun componentMapField() = mHookInfo.autoLike.kingPositionComponent.componentMap.orNull\n\n    fun setMDataMethod() = mHookInfo.autoLike.storyAbsController.setMData.orNull\n\n    fun getMPlayerMethod() = mHookInfo.autoLike.storyAbsController.getMPlayer.orNull\n\n    fun themeName() = mHookInfo.themeName.field.orNull\n\n    fun downloadingThread() = mHookInfo.downloadThread.field.orNull\n\n    fun reportDownloadThread() = mHookInfo.downloadThread.reportDownload.method.orNull\n\n    fun openDrawer() = mHookInfo.drawer.open.orNull\n\n    fun closeDrawer() = mHookInfo.drawer.close.orNull\n\n    fun isDrawerOpen() = mHookInfo.drawer.isOpen.orNull\n\n    fun paramsToMap() = mHookInfo.bangumiParams.paramsToMap.orNull\n\n    fun gson() = mHookInfo.gsonHelper.gson.orNull\n\n    fun getPlaybackSpeed() = mHookInfo.playerCoreService.getPlaybackSpeed.orNull\n\n    fun urlField() = mHookInfo.okHttp.request.url.orNull\n\n    fun gsonToJson() = mHookInfo.gsonHelper.toJson.orNull\n\n    fun gsonFromJson() = mHookInfo.gsonHelper.fromJson.orNull\n\n    fun pegasusFeed() = mHookInfo.pegasusFeed.method.orNull\n\n    fun descCopy() = mHookInfo.descCopy.methodsList.map { it.orNull }\n\n    fun descCopyView() = mHookInfo.descCopy.classesList.map { it from mClassLoader }\n\n    fun comment3Copy() = mHookInfo.comment3Copy.method.orNull\n\n    fun comment3ViewIndex() = mHookInfo.comment3Copy.comment3ViewIndex\n\n    fun responseDataField() = runCatchingOrNull {\n        rxGeneralResponseClass?.getDeclaredField(\"data\")?.name\n    } ?: \"_data\"\n\n    fun okio() = mHookInfo.okio.field.orNull\n\n    fun okioLength() = mHookInfo.okio.length.orNull\n\n    fun okioInputStream() = mHookInfo.okioBuffer.inputStream.orNull\n\n    fun okioReadFrom() = mHookInfo.okioBuffer.readFrom.orNull\n\n    fun seekTo() = mHookInfo.playerCoreService.seekTo.orNull\n\n    fun onSeekComplete() = mHookInfo.playerCoreService.onSeekComplete.orNull\n\n    fun kanbanCallback() = mHookInfo.kanBan.method.orNull\n\n    fun showToast() = mHookInfo.toastHelper.show.orNull\n\n    fun cancelShowToast() = mHookInfo.toastHelper.cancel.orNull\n\n    fun canTrialMethod() = mHookInfo.vipQualityTrialService.canTrial.orNull\n\n    fun setInvalidTips() = commentInvalidFragmentClass?.declaredMethods?.find { m ->\n        m.parameterTypes.let { it.size == 2 && it[0] == commentInvalidFragmentClass && it[1].name == \"kotlin.Pair\" }\n    }?.name\n\n    fun create() = mHookInfo.okHttp.responseBody.create.orNull\n\n    fun string() = mHookInfo.okHttp.responseBody.string.orNull\n\n    fun get() = mHookInfo.okHttp.mediaType.get.orNull\n\n    fun setParser() = mHookInfo.biliCall.setParser.orNull\n\n    fun biliCallRequestField() = mHookInfo.biliCall.request.orNull\n\n    fun onOperateClick() = mHookInfo.onOperateClick.orNull\n\n    fun getContentString() = mHookInfo.getContentString.orNull\n\n    fun check() = mHookInfo.updateInfoSupplier.check.orNull\n\n    fun dynamicDescHolderListeners() =\n        mHookInfo.dynDescHolderListenerList.map { it.from(mClassLoader) }\n\n    fun playerFullStoryWidgets() =\n        mHookInfo.playerFullStoryWidgetList.map { it.class_.from(mClassLoader) to it.method.orNull }\n\n    fun bangumiUniformSeasonActivityEntrance() = mHookInfo.bangumiSeasonActivityEntrance.orNull\n\n    fun getPreload() = mHookInfo.playerPreloadHolder.get.orNull\n\n    fun playerQualityServices() =\n        mHookInfo.playerQualityServiceList.map { it.class_.from(mClassLoader) to it.getDefaultQnThumb.orNull }\n\n    fun getDefaultQn() = mHookInfo.playerSettingHelper.getDefaultQn.orNull\n\n    fun selectQuality() = mHookInfo.qualityStrategyProvider.selectQuality.orNull\n\n    fun liveRtcEnable() = mHookInfo.liveRtcHelper.liveRtcEnableMethod.orNull\n\n    fun allThemesField() = mHookInfo.builtInThemes.all.orNull\n\n    fun getBLKVPrefs() = mHookInfo.biliGlobalPreference.get.orNull\n\n    fun onFeedClicked() = mHookInfo.cardClickProcessor.onFeedClicked.orNull\n\n    fun buildSelectorDataMethod() = mHookInfo.liveQuality.selectUtil.buildSelectorData.orNull\n\n    fun switchAutoMethod() = mHookInfo.liveQuality.sourceService.switchAuto.orNull\n\n    fun interceptMethod() = mHookInfo.liveQuality.interceptor.intercept.orNull\n\n    fun httpUrlParseMethod() = mHookInfo.okHttp.httpUrl.parse.orNull\n\n    fun getPreBuiltConfigMethod() = mHookInfo.preBuiltConfig.get.orNull\n\n    fun getDataSPMethod() = mHookInfo.dataSP.get.orNull\n\n    fun buildCommonResolverParamsMethod() =\n        mHookInfo.resolveClientCompanion.buildCommonResolverParams.orNull\n\n    fun setExtraContentMethod() = mHookInfo.gCommonResolverParams.setExtraContent.orNull\n\n    fun rewardFlag() = mHookInfo.rewardAd.rewardFlag.orNull\n\n    fun addVideo() = mHookInfo.storyPagerPlayer.addVideo.orNull\n\n    private fun readHookInfo(context: Context): Configs.HookInfo {\n        try {\n            val hookInfoFile = File(context.cacheDir, Constant.HOOK_INFO_FILE_NAME)\n            Log.d(\"Reading hook info: $hookInfoFile\")\n            val t = measureTimeMillis {\n                if (hookInfoFile.isFile && hookInfoFile.canRead()) {\n                    val lastUpdateTime = context.packageManager.getPackageInfo(\n                        AndroidAppHelper.currentPackageName(),\n                        0\n                    ).lastUpdateTime\n                    val lastModuleUpdateTime = try {\n                        context.packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, 0)\n                    } catch (e: Throwable) {\n                        null\n                    }?.lastUpdateTime ?: 0\n                    val info = FileInputStream(hookInfoFile).use {\n                        runCatchingOrNull { Configs.HookInfo.parseFrom(it) }\n                            ?: Configs.HookInfo.newBuilder().build()\n                    }\n                    if (info.lastUpdateTime >= lastUpdateTime && info.lastUpdateTime >= lastModuleUpdateTime\n                        && getVersionCode(context.packageName) == info.clientVersionCode\n                        && BuildConfig.VERSION_CODE == info.moduleVersionCode\n                        && BuildConfig.VERSION_NAME == info.moduleVersionName\n                        && info.biliAccounts.getAccessKey.orNull != null\n                    )\n                        return info\n                }\n            }\n            Log.d(\"Read hook info completed: take $t ms\")\n        } catch (e: Throwable) {\n            Log.w(e)\n        }\n        return initHookInfo(context).also {\n            try {\n                val hookInfoFile = File(context.cacheDir, Constant.HOOK_INFO_FILE_NAME)\n                if (hookInfoFile.exists()) hookInfoFile.delete()\n                FileOutputStream(hookInfoFile).use { o -> it.writeTo(o) }\n            } catch (e: Exception) {\n                Log.e(e)\n            }\n        }\n    }\n\n    companion object {\n        @JvmStatic\n        fun findRealClassloader(classloader: BaseDexClassLoader): BaseDexClassLoader {\n            val serviceField = classloader.javaClass.findFirstFieldByExactTypeOrNull(\n                \"com.bilibili.lib.tribe.core.internal.loader.DefaultClassLoaderService\" from classloader\n            )\n            val delegateField = classloader.javaClass.findFirstFieldByExactTypeOrNull(\n                \"com.bilibili.lib.tribe.core.internal.loader.TribeLoaderDelegate\" from classloader\n            )\n            return if (serviceField != null) {\n                serviceField.type.declaredFields.filter { f ->\n                    f.type == ClassLoader::class.java\n                }.map { f ->\n                    classloader.getObjectFieldOrNull(serviceField.name)\n                        ?.getObjectFieldOrNull(f.name)\n                }.firstOrNull { o ->\n                    o?.javaClass?.name?.startsWith(\"com.bilibili\") == false\n                } as? BaseDexClassLoader ?: classloader\n            } else if (delegateField != null) {\n                val loaderField =\n                    delegateField.type.findFirstFieldByExactTypeOrNull(ClassLoader::class.java)\n                val out = classloader.getObjectFieldOrNull(delegateField.name)\n                    ?.getObjectFieldOrNull(loaderField?.name)\n                if (BaseDexClassLoader::class.java.isInstance(out)) out as BaseDexClassLoader else classloader\n            } else classloader\n        }\n\n        @JvmStatic\n        fun initHookInfo(context: Context) = hookInfo {\n            val classloader = context.classLoader\n            val classesList = context.classLoader.allClassesList(::findRealClassloader).asSequence()\n\n            try {\n                System.loadLibrary(\"biliroaming\")\n            } catch (e: Throwable) {\n                Log.e(e)\n                Log.toast(\"不支持该架构或框架，部分功能可能失效\")\n                return@hookInfo\n            }\n\n            val dexHelper =\n                DexHelper(classloader.findDexClassLoader(::findRealClassloader) ?: return@hookInfo)\n            lastUpdateTime = max(\n                context.packageManager.getPackageInfo(\n                    AndroidAppHelper.currentPackageName(),\n                    0\n                ).lastUpdateTime,\n                runCatchingOrNull {\n                    context.packageManager.getPackageInfo(\n                        BuildConfig.APPLICATION_ID,\n                        0\n                    )\n                }?.lastUpdateTime ?: 0\n            )\n            clientVersionCode = getVersionCode(context.packageName)\n            moduleVersionCode = BuildConfig.VERSION_CODE\n            moduleVersionName = BuildConfig.VERSION_NAME\n            mapIds = mapIds {\n                val reg = Regex(\"^tv\\\\.danmaku\\\\.bili\\\\.[^.]*$\")\n                val mask = Modifier.STATIC or Modifier.PUBLIC or Modifier.FINAL\n                classesList.filter {\n                    it.startsWith(\"tv.danmaku.bili\")\n                }.filter {\n                    it.matches(reg)\n                }.flatMap { c ->\n                    c.findClass(classloader).declaredFields.filter {\n                        it.modifiers == mask\n                                && it.type == Int::class.javaPrimitiveType\n                    }\n                }.forEach { ids[it.name] = it.get(null) as Int }\n            }\n\n            bangumiApiResponse = class_ {\n                name = \"com.bilibili.bangumi.data.common.api.BangumiApiResponse\"\n            }\n            retrofitResponse = class_ {\n                name = \"retrofit2.Response\".from(classloader)?.name\n                    ?: dexHelper.findMethodUsingString(\n                        \"rawResponse must be successful response\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }?.declaringClass?.name ?: return@class_\n            }\n            val responseBodyClass = \"okhttp3.ResponseBody\".from(classloader)\n                ?: dexHelper.findMethodUsingString(\n                    \"Cannot buffer entire body for content length: \",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)\n                }.firstOrNull {\n                    it.declaringClass?.name?.startsWith(\"okhttp3\") == true\n                }?.declaringClass\n            okHttp = okHttp {\n                val responseClass = \"okhttp3.Response\".from(classloader)\n                    ?: dexHelper.findMethodUsingString(\n                        \"Response{protocol=\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        false\n                    ).asSequence().mapNotNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }.firstOrNull {\n                        it.declaringClass?.name?.startsWith(\"okhttp3\") == true\n                    }?.declaringClass ?: return@okHttp\n                val requestClass = \"okhttp3.Request\".from(classloader)\n                    ?: dexHelper.findMethodUsingString(\n                        \"Request{method=\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        false\n                    ).asSequence().mapNotNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }.firstOrNull {\n                        it.declaringClass?.name?.startsWith(\"okhttp3\") == true\n                    }?.declaringClass ?: return@okHttp\n                val urlClass = \"okhttp3.HttpUrl\".from(classloader)\n                    ?: dexHelper.findMethodUsingString(\n                        \":@\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        false\n                    ).asSequence().mapNotNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }.firstOrNull {\n                        it.declaringClass?.name?.startsWith(\"okhttp3\") == true\n                    }?.declaringClass ?: return@okHttp\n                val parseMethod = urlClass.declaredMethods.firstOrNull {\n                    it.isStatic && it.returnType == urlClass && it.parameterCount == 1 && it.parameterTypes[0] == String::class.java\n                } ?: return@okHttp\n                responseBodyClass ?: return@okHttp\n                val getMethod = dexHelper.findMethodUsingString(\n                    \"No subtype found for:\",\n                    true,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)\n                }.firstOrNull {\n                    it.declaringClass?.name?.startsWith(\"okhttp3\") == true\n                } ?: return@okHttp\n                request = request {\n                    class_ = class_ { name = requestClass.name }\n                    url = field {\n                        name = requestClass.findFirstFieldByExactTypeOrNull(urlClass)?.name\n                            ?: return@field\n                    }\n                }\n                response = response {\n                    class_ = class_ { name = responseClass.name }\n                    request = field {\n                        name = responseClass.findFirstFieldByExactTypeOrNull(requestClass)?.name\n                            ?: return@field\n                    }\n                }\n                responseBody = responseBody {\n                    class_ = class_ { name = responseBodyClass.name }\n                    create = method {\n                        name = responseBodyClass.methods.find {\n                            it.isStatic && it.parameterTypes.size == 2 && it.parameterTypes[1] == String::class.java\n                        }?.name ?: return@method\n                    }\n                    string = method {\n                        name = responseBodyClass.declaredMethods.find {\n                            it.parameterTypes.isEmpty() && it.returnType == String::class.java\n                        }?.name ?: return@method\n                    }\n                }\n                mediaType = mediaType {\n                    class_ = class_ { name = getMethod.declaringClass.name }\n                    get = method { name = getMethod.name }\n                }\n                httpUrl = httpUrl {\n                    class_ = class_ { name = urlClass.name }\n                    parse = method { name = parseMethod.name }\n                }\n            }\n            fastJson = fastJson {\n                val fastJsonClass = dexHelper.findMethodUsingString(\n                    \"toJSON error\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@fastJson\n                val notObfuscated = \"JSON\" == fastJsonClass.simpleName\n                class_ = class_ { name = fastJsonClass.name }\n                parse = method { name = if (notObfuscated) \"parseObject\" else \"a\" }\n            }\n            biliAccounts = biliAccounts {\n                val biliAccountsClass = dexHelper.findMethodUsingString(\n                    \"logout with account exception\",\n                    false,\n                    -1,\n                    1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@biliAccounts\n                class_ = class_ { name = biliAccountsClass.name }\n                val biliAccountIndex = dexHelper.encodeClassIndex(biliAccountsClass)\n                val biliAuthFragmentMethodIndex = dexHelper.findMethodUsingString(\n                    \"initFacial enter\",\n                    true,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull() ?: return@biliAccounts\n                val calledMethods = dexHelper.findMethodInvoking(\n                    biliAuthFragmentMethodIndex,\n                    -1,\n                    -1,\n                    null,\n                    biliAccountIndex,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it) as? Method\n                }\n\n                get = method {\n                    name = calledMethods.firstOrNull {\n                        it.isStatic && it.parameterTypes.size == 1 && it.parameterTypes[0] == Context::class.java && it.returnType == biliAccountsClass\n                    }?.name ?: return@method\n                }\n\n                getAccessKey = method {\n                    name = calledMethods.firstOrNull {\n                        it.isNotStatic && it.parameterTypes.isEmpty() && it.returnType == String::class.java\n                    }?.name ?: return@method\n                }\n            }\n            themeHelper = themeHelper {\n                val colorArrayMethod = dexHelper.findMethodUsingString(\n                    \"theme_entries_last_key\",\n                    false,\n                    dexHelper.encodeClassIndex(Int::class.java),\n                    1,\n                    null,\n                    -1,\n                    longArrayOf(dexHelper.encodeClassIndex(Context::class.java)),\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@themeHelper\n\n                class_ = class_ { name = colorArrayMethod.declaringClass.name }\n                colorArray = field { name = colorArrayMethod.name }\n            }\n            themeIdHelper = themeIdHelper {\n                val mWebActivityClass = \"tv.danmaku.bili.ui.webview.MWebActivity\".from(classloader)\n                    ?: return@themeIdHelper\n                val mWebActivityIndex = dexHelper.encodeClassIndex(mWebActivityClass)\n                val themeIdHelperClass = dexHelper.findMethodUsingString(\n                    \"native.theme\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    mWebActivityIndex,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.run {\n                    dexHelper.findMethodInvoking(\n                        this,\n                        -1,\n                        0,\n                        \"I\",\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).firstOrNull()?.let {\n                        dexHelper.decodeMethodIndex(it)\n                    }?.declaringClass\n                } ?: return@themeIdHelper\n                class_ = class_ { name = themeIdHelperClass.name }\n                colorId = field {\n                    name = themeIdHelperClass.declaredFields.find {\n                        it.type == SparseArray::class.java\n                    }?.name ?: return@field\n                }\n            }\n            columnHelper = columnHelper {\n                val columnHelperClass = classesList.filter {\n                    it.startsWith(\"com.bilibili.column.helper\")\n                }.firstNotNullOfOrNull { c ->\n                    c.findClass(classloader).takeIf {\n                        it.declaredFields.count { f ->\n                            Modifier.isStatic(it.modifiers) && f.type == SparseArray::class.java\n                        } > 1\n                    }\n                } ?: return@columnHelper\n                class_ = class_ {\n                    name = columnHelperClass.name ?: return@class_\n                }\n                colorArray = field {\n                    name = columnHelperClass.declaredFields.firstOrNull {\n                        it.type == SparseArray::class.java &&\n                                (it.genericType as ParameterizedType).actualTypeArguments[0].toString() == \"int[]\"\n                    }?.name ?: return@field\n                }\n            }\n            skinList = method {\n                val biliSkinListClass =\n                    \"tv.danmaku.bili.ui.theme.api.BiliSkinList\".findClassOrNull(classloader)\n                        ?: return@method\n                name =\n                    \"tv.danmaku.bili.ui.theme.ThemeStoreActivity\".findClassOrNull(classloader)?.declaredMethods?.firstOrNull {\n                        it.parameterTypes.size == 2 && it.parameterTypes[0] == biliSkinListClass &&\n                                it.parameterTypes[1] == Boolean::class.javaPrimitiveType\n                    }?.name ?: return@method\n            }\n            themeProcessor = themeProcessor {\n                val biliSkinListClass =\n                    \"tv.danmaku.bili.ui.theme.api.BiliSkinList\".findClassOrNull(classloader)\n                val themeProcessorClass = classesList.filter {\n                    it.startsWith(\"tv.danmaku.bili.ui.theme\")\n                }.firstNotNullOfOrNull { c ->\n                    c.findClass(classloader).takeIf {\n                        it.declaredFields.count { f ->\n                            f.type == biliSkinListClass\n                        } > 1\n                    }\n                } ?: return@themeProcessor\n                class_ = class_ { name = themeProcessorClass.name }\n                methods += themeProcessorClass.declaredMethods.filter {\n                    it.parameterTypes.isEmpty() && it.modifiers == 0\n                }.map { method { name = it.name } }\n            }\n            themeListClick = class_ {\n                name =\n                    \"tv.danmaku.bili.ui.theme.ThemeStoreActivity\".findClassOrNull(classloader)?.declaredClasses?.firstOrNull {\n                        it.interfaces.contains(View.OnClickListener::class.java)\n                    }?.name ?: return@class_\n            }\n            themeName = themeName {\n                val mainGarbClass = dexHelper.findMethodUsingString(\n                    \".garb.GARB_CHANGE\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                }.firstOrNull() ?: return@themeName\n                val id2NameIndex = dexHelper.findMethodUsingString(\n                    \"white\",\n                    false,\n                    -1,\n                    1,\n                    null,\n                    dexHelper.encodeClassIndex(mainGarbClass),\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstOrNull() ?: return@themeName\n                val themeNameClass = dexHelper.findMethodInvoking(\n                    id2NameIndex,\n                    dexHelper.encodeClassIndex(Map::class.java),\n                    0,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                }.firstOrNull() ?: return@themeName\n                class_ = class_ {\n                    name = themeNameClass.name\n                }\n                field = field {\n                    name = themeNameClass.declaredFields.firstOrNull {\n                        it.type == Map::class.java\n                                && Modifier.isStatic(it.modifiers)\n                    }?.name ?: return@field\n                }\n            }\n            autoLike = autoLike {\n                kingPositionComponent = kingPositionComponent {\n                    val kingPositionComponentClass = dexHelper.findMethodUsingString(\n                        \"LikeClicked(view=\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)?.declaringClass?.run {\n                            var outerClass = this\n                            while (outerClass.declaringClass != null) {\n                                outerClass = outerClass.declaringClass!!\n                            }\n                            outerClass\n                        }\n                    } ?: return@kingPositionComponent\n                    val componentMapField = kingPositionComponentClass.declaredFields.firstOrNull {\n                        it.type == Map::class.java\n                    } ?: return@kingPositionComponent\n                    class_ = class_ { name = kingPositionComponentClass.name }\n                    componentMap = field { name = componentMapField.name }\n                }\n                storyAbsController = storyAbsController {\n                    val storyAbsControllerClass = dexHelper.findMethodUsingString(\n                        \"notify player state fail\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().mapNotNull {\n                        dexHelper.decodeMethodIndex(it)?.declaringClass\n                    }.firstOrNull() ?: return@storyAbsController\n                    val storyPagerPlayerInterface = dexHelper.findMethodUsingString(\n                        \" play item but is inactivate\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().mapNotNull {\n                        dexHelper.decodeMethodIndex(it)?.declaringClass?.interfaces?.getOrNull(0)\n                    }.firstOrNull() ?: return@storyAbsController\n                    val storyDetailClass = \"com.bilibili.video.story.StoryDetail\".from(classloader)\n                    val setMDataMethod = storyAbsControllerClass.declaredMethods.firstOrNull {\n                        it.returnType == Void.TYPE && it.parameterCount == 1 && it.parameterTypes[0] == storyDetailClass\n                    } ?: return@storyAbsController\n                    val getMPlayerMethod = storyAbsControllerClass.declaredMethods.firstOrNull {\n                        storyPagerPlayerInterface.isAssignableFrom(it.returnType) && it.parameterCount == 0\n                    } ?: return@storyAbsController\n                    class_ = class_ { name = storyAbsControllerClass.name }\n                    setMData = method { name = setMDataMethod.name }\n                    getMPlayer = method { name = getMPlayerMethod.name }\n                }\n            }\n            signQuery = signQuery {\n                val signedQueryClass =\n                    \"com.bilibili.nativelibrary.SignedQuery\".findClassOrNull(classloader)\n                        ?: return@signQuery\n                val libBiliClass =\n                    \"com.bilibili.nativelibrary.LibBili\" from classloader ?: return@signQuery\n                class_ = class_ { name = libBiliClass.name }\n                method = method {\n                    name = libBiliClass.declaredMethods.firstOrNull {\n                        it.parameterTypes.size == 1 && it.parameterTypes[0] == Map::class.java &&\n                                it.returnType == signedQueryClass\n                    }?.name ?: return@method\n                }\n            }\n            settings = settings {\n                val menuGroupItemClass =\n                    \"com.bilibili.lib.homepage.mine.MenuGroup\\$Item\" from classloader\n                        ?: return@settings\n                menuGroupItem = class_ { name = menuGroupItemClass.name }\n                settingRouter = class_ {\n                    name = dexHelper.findMethodUsingString(\n                        \"UperHotMineSolution\",\n                        false,\n                        -1,\n                        0,\n                        \"V\",\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }?.declaringClass?.interfaces?.firstOrNull()?.let {\n                        dexHelper.encodeClassIndex(it)\n                    }?.let {\n                        dexHelper.findField(it, null, true).asSequence().firstNotNullOfOrNull { f ->\n                            dexHelper.decodeFieldIndex(f)\n                        }?.declaringClass\n                    }?.name ?: return@class_\n                }\n                val contextIndex = dexHelper.encodeClassIndex(Context::class.java)\n                val listIndex = dexHelper.encodeClassIndex(List::class.java)\n                dexHelper.findMethodUsingString(\n                    \"main.my-information.noportrait.0.show\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull { dexHelper.decodeMethodIndex(it)?.declaringClass }\n                    .forEach { homeUserCenterClass ->\n                        val homeUserCenterIndex = dexHelper.encodeClassIndex(homeUserCenterClass)\n                        val addSettingMethod = dexHelper.findMethodUsingString(\n                            \"bilibili://main/scan\",\n                            true,\n                            -1,\n                            -1,\n                            null,\n                            homeUserCenterIndex,\n                            null,\n                            longArrayOf(contextIndex),\n                            null,\n                            false\n                        ).asSequence().mapNotNull {\n                            dexHelper.decodeMethodIndex(it) as? Method\n                        }.firstOrNull {\n                            it.parameterTypes.size == 2 &&\n                                    it.parameterTypes[1] != List::class.java\n                        } ?: dexHelper.findMethodUsingString(\n                            \"activity://main/preference\",\n                            true,\n                            -1,\n                            -1,\n                            null,\n                            homeUserCenterIndex,\n                            null,\n                            longArrayOf(contextIndex, listIndex),\n                            null,\n                            true\n                        ).asSequence().firstNotNullOfOrNull {\n                            dexHelper.decodeMethodIndex(it)\n                        } ?: dexHelper.findMethodUsingString(\n                            \"bilibili://main/preference\",\n                            true,\n                            -1,\n                            -1,\n                            null,\n                            homeUserCenterIndex,\n                            null,\n                            longArrayOf(contextIndex, listIndex),\n                            null,\n                            true\n                        ).asSequence().firstNotNullOfOrNull {\n                            dexHelper.decodeMethodIndex(it)\n                        } ?: return@settings\n                        homeUserCenter += homeUserCenter {\n                            class_ = class_ { name = homeUserCenterClass.name }\n                            addSetting = method { name = addSettingMethod.name }\n                        }\n                    }\n            }\n            drawer = drawer {\n                val navigationViewClass =\n                    \"android.support.design.widget.NavigationView\" from classloader\n                val regex = Regex(\"^tv\\\\.danmaku\\\\.bili\\\\.ui\\\\.main2\\\\.[^.]*$\")\n                class_ = class_ {\n                    name = classesList.filter {\n                        it.startsWith(\"tv.danmaku.bili.ui.main2\")\n                    }.filter { it.matches(regex) }.firstOrNull { c ->\n                        c.findClass(classloader).declaredFields.any {\n                            it.type == navigationViewClass\n                        }\n                    } ?: return@class_\n                }\n                val drawerLayoutClass = \"androidx.drawerlayout.widget.DrawerLayout\" from classloader\n                    ?: \"android.support.v4.widget.DrawerLayout\" from classloader ?: return@drawer\n                layout = class_ { name = drawerLayoutClass.name }\n                layoutParams = class_ {\n                    name = drawerLayoutClass.declaredClasses.firstOrNull {\n                        it.superclass == ViewGroup.MarginLayoutParams::class.java\n                    }?.name ?: return@class_\n                }\n                try {\n                    open = method {\n                        name = drawerLayoutClass.getMethod(\n                            \"openDrawer\",\n                            View::class.java,\n                            Boolean::class.javaPrimitiveType\n                        ).name ?: throw Throwable()\n                    }\n                    close = method {\n                        name = drawerLayoutClass.getMethod(\n                            \"closeDrawer\",\n                            View::class.java,\n                            Boolean::class.javaPrimitiveType\n                        ).name ?: throw Throwable()\n                    }\n                } catch (e: Throwable) {\n                    val a = drawerLayoutClass.declaredMethods.filter {\n                        Modifier.isPublic(it.modifiers) &&\n                                it.parameterTypes.size == 2 && it.parameterTypes[0] == View::class.java &&\n                                it.parameterTypes[1] == Boolean::class.javaPrimitiveType\n                    }.map { it.name }.toTypedArray()\n                    if (a.size == 2) {\n                        open = method { name = a[0] }\n                        close = method { name = a[1] }\n                    }\n                }\n                isOpen = method {\n                    name = runCatchingOrNull {\n                        drawerLayoutClass.getMethod(\"isDrawerOpen\", View::class.java).name\n                    } ?: drawerLayoutClass.declaredMethods.firstOrNull {\n                        Modifier.isPublic(it.modifiers) &&\n                                it.parameterTypes.size == 1 && it.parameterTypes[0] == View::class.java &&\n                                it.returnType == Boolean::class.javaPrimitiveType\n                    }?.name ?: return@method\n                }\n            }\n            downloadThread = downloadThread {\n                val viewHostClass = (if (platform == \"android_hd\")\n                    \"tv.danmaku.bili.ui.offline.HdOfflineDowningFragment\".from(classloader)\n                else\n                    \"tv.danmaku.bili.ui.offline.DownloadingActivity\".from(classloader))\n                    ?: return@downloadThread\n                viewHost = class_ { name = viewHostClass.name }\n                field = field {\n                    name = viewHostClass.declaredFields.find {\n                        it.type == Int::class.javaPrimitiveType\n                    }?.name ?: return@downloadThread\n                }\n                val onTaskCountClickMethod = viewHostClass.declaredMethods.find { m ->\n                    m.isSynthetic && m.parameterTypes.let {\n                        it.size == 4 && if (platform == \"android_hd\") {\n                            it[0] == TextView::class.java && it[1] == viewHostClass\n                        } else it[0] == viewHostClass && it[1] == TextView::class.java\n                    }\n                } ?: return@downloadThread\n                val listenerClass = dexHelper.findMethodInvoked(\n                    dexHelper.encodeMethodIndex(onTaskCountClickMethod),\n                    -1,\n                    1,\n                    \"VL\",\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@downloadThread\n                listener = class_ { name = listenerClass.name }\n                val reportMethod = dexHelper.findMethodUsingString(\n                    \"meantime\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    dexHelper.encodeClassIndex(viewHostClass),\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.run {\n                    dexHelper.findMethodInvoking(\n                        this,\n                        -1,\n                        2,\n                        \"VLI\",\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).firstOrNull()?.let {\n                        dexHelper.decodeMethodIndex(it)\n                    }\n                } ?: return@downloadThread\n                reportDownload = reportDownload {\n                    class_ = class_ { name = reportMethod.declaringClass.name }\n                    method = method { name = reportMethod.name }\n                }\n            }\n            bangumiParams = bangumiParams {\n                val bangumiParamsClass = dexHelper.findMethodUsingString(\n                    \"UniformSeasonParams(\",\n                    true,\n                    -1,\n                    0,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@bangumiParams\n                class_ = class_ { name = bangumiParamsClass.name }\n                paramsToMap = method {\n                    name = bangumiParamsClass.declaredMethods.firstOrNull {\n                        it.returnType == java.util.Map::class.java\n                    }?.name ?: return@method\n                }\n            }\n            gsonHelper = gsonHelper {\n                val gsonClass = dexHelper.findMethodUsingString(\n                    \"AssertionError (GSON\",\n                    true,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@gsonHelper\n                val gsonConverterClass =\n                    dexHelper.findField(dexHelper.encodeClassIndex(gsonClass), null, false)\n                        .asSequence().firstNotNullOfOrNull {\n                            dexHelper.decodeFieldIndex(it)?.takeIf { f ->\n                                f.isStatic && f.isFinal && f.isPublic\n                            }?.declaringClass?.takeIf { c ->\n                                c.declaredMethods.any { m ->\n                                    m.returnType == gsonClass && m.isNotStatic\n                                }\n                            }\n                        } ?: return@gsonHelper\n                gsonConverter = class_ { name = gsonConverterClass.name }\n                gson = field {\n                    name = gsonConverterClass.declaredMethods.firstOrNull { m ->\n                        m.returnType == gsonClass && m.isNotStatic\n                    }?.name ?: return@field\n                }\n                toJson = method {\n                    name = gsonClass.declaredMethods.firstOrNull { m ->\n                        m.returnType == String::class.java && m.parameterTypes.size == 1 && m.parameterTypes[0] == Object::class.java\n                    }?.name ?: return@method\n                }\n                fromJson = method {\n                    name = gsonClass.declaredMethods.firstOrNull { m ->\n                        m.returnType == Object::class.java && m.parameterTypes.size == 2 && m.parameterTypes[0] == String::class.java && m.parameterTypes[1] == Class::class.java\n                    }?.name ?: return@method\n                }\n            }\n            playerCoreService = playerCoreService {\n                val seekToMethod = dexHelper.findMethodUsingString(\n                    \"[player]seek to\",\n                    true,\n                    -1,\n                    1,\n                    \"VI\",\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: run {\n                    val doSeekToIndex = dexHelper.findMethodUsingString(\n                        \"[player]seek to\",\n                        true,\n                        -1,\n                        2,\n                        \"VIZ\",\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).firstOrNull() ?: return@playerCoreService\n                    val doSeekToMethod =\n                        dexHelper.decodeMethodIndex(doSeekToIndex) ?: return@playerCoreService\n                    val playerCoreServiceIndex =\n                        dexHelper.encodeClassIndex(doSeekToMethod.declaringClass)\n                    dexHelper.findMethodInvoked(\n                        doSeekToIndex,\n                        -1,\n                        1,\n                        \"VI\",\n                        playerCoreServiceIndex,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)\n                    } ?: run {\n                        dexHelper.findMethodInvoked(\n                            doSeekToIndex,\n                            -1,\n                            2,\n                            \"VIZ\",\n                            playerCoreServiceIndex,\n                            null,\n                            null,\n                            null,\n                            true\n                        ).asSequence().firstNotNullOfOrNull {\n                            dexHelper.decodeMethodIndex(it)\n                        }\n                    } ?: doSeekToMethod\n                }\n                val playerCoreServiceClass = seekToMethod.declaringClass\n                seekTo = method { name = seekToMethod.name }\n                class_ = class_ { name = playerCoreServiceClass.name }\n                val onSeekCompleteMethod = dexHelper.findMethodUsingString(\n                    \"[player]seek complete\",\n                    true,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@playerCoreService\n                onSeekComplete = method { name = onSeekCompleteMethod.name }\n                seekCompleteListener =\n                    class_ { name = onSeekCompleteMethod.declaringClass.name }\n                getPlaybackSpeed = method {\n                    name = dexHelper.findMethodUsingString(\n                        \"player_key_video_speed\",\n                        true,\n                        -1,\n                        1,\n                        \"FZ\",\n                        dexHelper.encodeClassIndex(playerCoreServiceClass),\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }?.name ?: return@method\n                }\n            }\n            val playSpeedManagerClass = (\"com.bilibili.player.tangram.basic.PlaySpeedManagerImpl\" from classloader) ?: run {\n                val pcsFacadeClass = dexHelper.findMethodUsingString(\n                        \"Cannot switch to quality \",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@run null\n\n                val playSpeedManagerInterface = pcsFacadeClass.declaredFields.firstNotNullOfOrNull { f ->\n                    if (f.type.isInterface && f.type.declaredMethods.size == 1) f.type else null\n                }\n                classesList.filter {\n                    it.startsWith(\"com.bilibili.player.tangram\")\n                }.firstNotNullOfOrNull { c ->\n                    c.findClass(classloader).takeIf { it.interfaces.contains(playSpeedManagerInterface) }\n                }\n            }\n            playSpeedManager = class_ {\n                name = playSpeedManagerClass?.name ?: return@class_\n            }\n            generalResponse = class_ {\n                name = \"com.bilibili.okretro.GeneralResponse\"\n            }\n            pegasusFeed = pegasusFeed {\n                val fastJSONObject =\n                    dexHelper.encodeClassIndex(\n                        \"com.alibaba.fastjson.JSONObject\" from classloader ?: return@pegasusFeed\n                    )\n                val pegasusFeedClass = dexHelper.findMethodUsingString(\n                    \"card_type is empty\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    longArrayOf(fastJSONObject),\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@pegasusFeed\n                class_ = class_ { name = pegasusFeedClass.name }\n                method = method {\n                    name = pegasusFeedClass.declaredMethods.firstOrNull {\n                        it.parameterTypes.size == 1 && it.parameterTypes[0].name == this@hookInfo.okHttp.responseBody.class_.name\n                                && it.returnType != Object::class.java\n                    }?.name ?: return@method\n                }\n            }\n            popular = popular {\n                class_ = class_ { name = \"com.bapis.bilibili.app.show.popular.v1.PopularMoss\" }\n                method = method {\n                    name = \"index\"\n                }\n            }\n            chronosSwitch = class_ {\n                val regex = Regex(\"\"\"^tv\\.danmaku\\.chronos\\.wrapper\\.[^.]*$\"\"\")\n                name = classesList.filter {\n                    it.matches(regex)\n                }.firstOrNull { c ->\n                    c.findClass(classloader).declaredFields.count {\n                        it.type == Boolean::class.javaObjectType\n                    } >= 5\n                } ?: return@class_\n            }\n            subtitleSpan = class_ {\n                name = classesList.filter {\n                    it.startsWith(\"tv.danmaku.danmaku.subititle\") ||\n                            it.startsWith(\"tv.danmaku.danmaku.subtitle\")\n                }.firstOrNull { c ->\n                    c.findClass(classloader).interfaces.contains(LineBackgroundSpan::class.java)\n                } ?: return@class_\n            }\n            val commentSpanClass = \"com.bilibili.app.comm.comment2.widget.CommentExpandableTextView\"\n                .from(classloader) ?: dexHelper.findMethodUsingString(\n                \"comment.catch_on_draw_exception\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                true\n            ).asSequence().firstNotNullOfOrNull {\n                dexHelper.decodeMethodIndex(it)\n            }?.declaringClass\n            commentSpan = class_ {\n                name = commentSpanClass?.name ?: return@class_\n            }\n            val viewIndex = dexHelper.encodeClassIndex(View::class.java)\n            commentLongClick = class_ {\n                val onLongClickListenerIndex = dexHelper.encodeMethodIndex(\n                    View::class.java.getDeclaredMethod(\n                        \"setOnLongClickListener\",\n                        View.OnLongClickListener::class.java\n                    )\n                )\n                name = dexHelper.findMethodInvoked(\n                    onLongClickListenerIndex,\n                    -1,\n                    2,\n                    \"VLL\",\n                    -1,\n                    longArrayOf(viewIndex, -1),\n                    null,\n                    null,\n                    false\n                ).firstOrNull {\n                    dexHelper.decodeMethodIndex(it)?.run {\n                        this !is Constructor<*> && isStatic && isPublic && (this as? Method)?.parameterTypes.let { t ->\n                            t?.get(0) == View::class.java && t[1] != CharSequence::class.java\n                        }\n                    } == true\n                }?.let {\n                    dexHelper.findMethodInvoking(it, -1, 2, \"VLL\", -1, null, null, null, false)\n                        .asSequence()\n                        .firstNotNullOfOrNull { m -> dexHelper.decodeMethodIndex(m) }?.declaringClass?.name\n                } ?: return@class_\n            }\n            commentLongClickNew = class_ {\n                val setExpandLinesMethod = commentSpanClass?.declaredMethods?.find {\n                    it.parameterTypes.size == 1 && it.parameterTypes[0] == Int::class.javaPrimitiveType\n                } ?: return@class_\n                val setExpandLinesIndex = dexHelper.encodeMethodIndex(setExpandLinesMethod)\n                name = dexHelper.findMethodInvoked(\n                    setExpandLinesIndex,\n                    -1,\n                    1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.findMethodInvoking(it, -1, 2, \"VLL\", -1, null, null, null, true)\n                        .map { m -> dexHelper.decodeMethodIndex(m) }\n                        .firstOrNull()?.declaringClass?.name\n                } ?: return@class_\n            }\n            okio = okIO {\n                val responseClassIndex =\n                    dexHelper.encodeClassIndex(responseBodyClass ?: return@okIO)\n                val createMethodIndex = dexHelper.findMethodUsingString(\n                    \"source == null\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    responseClassIndex,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull() ?: return@okIO\n                val wrapperClass = dexHelper.findMethodInvoking(\n                    createMethodIndex,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it).takeIf { m ->\n                        m is Constructor<*>\n                    }\n                }?.declaringClass ?: return@okIO\n                class_ = class_ { name = wrapperClass.name }\n                val okioField = wrapperClass.declaredFields.firstOrNull { f ->\n                    f.type.isAbstract\n                } ?: return@okIO\n                field = field { name = okioField.name }\n                val okioLenField = wrapperClass.declaredFields.firstOrNull { f ->\n                    f.type == Long::class.javaPrimitiveType\n                } ?: return@okIO\n                length = field { name = okioLenField.name }\n            }\n            okioBuffer = okIOBuffer {\n                val okioBufferClass = dexHelper.findMethodUsingString(\n                    \"already attached to a buffer\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)\n                }.firstNotNullOfOrNull {\n                    val method = it as? Method ?: return@firstNotNullOfOrNull null\n                    val firstParameterType = method.parameterTypes.firstOrNull()\n                    val returnType = method.returnType\n                    when {\n                        method.declaringClass?.name?.startsWith(\"okio\") != true -> null\n                        // https://github.com/square/okio/blob/okio-parent-2.0.0-RC1/okio/jvm/src/main/java/okio/Buffer.kt#L1717\n                        firstParameterType == null || firstParameterType == returnType -> it.declaringClass\n                        // https://github.com/square/okio/blob/parent-2.10.0/okio/src/commonMain/kotlin/okio/internal/Buffer.kt#L1509\n                        else -> firstParameterType\n                    }\n                } ?: return@okIOBuffer\n                class_ = class_ { name = okioBufferClass.name }\n                val okioInputStreamMethod = okioBufferClass.declaredMethods.firstOrNull {\n                    it.parameterTypes.isEmpty() && it.returnType == InputStream::class.java\n                } ?: return@okIOBuffer\n                inputStream = method { name = okioInputStreamMethod.name }\n                val okioReadFromMethod = okioBufferClass.declaredMethods.firstOrNull {\n                    it.parameterTypes.size == 1 && it.parameterTypes[0] == InputStream::class.java\n                } ?: return@okIOBuffer\n                readFrom = method { name = okioReadFromMethod.name }\n            }\n            classesList.filter {\n                it.startsWith(\"com.bilibili.bplus.followinglist.module.item\")\n                        && View.OnLongClickListener::class.java.isAssignableFrom(it.on(classloader))\n            }.let { l -> dynDescHolderListener.addAll(l.toList().map { class_ { name = it } }) }\n            descCopy = descCopy {\n                classesList.filter {\n                    it.startsWith(\"com.bilibili.ship.theseus.ugc.intro.ugcheadline.UgcIntroductionComponent\")\n                }.map { it.on(classloader) }.flatMap { c ->\n                    c.declaredMethods.filter {\n                        it.isPublic && it.parameterCount == 2 && it.parameterTypes[0] == View::class.java && it.parameterTypes[1] == ClickableSpan::class.java\n                    }\n                }.forEach {\n                    classes += class_ { name = it.declaringClass.name }\n                    methods += method { name = it.name }\n                }\n\n                val descViewHolderClass = dexHelper.findMethodUsingString(\n                    \"AV%d\",\n                    false,\n                    -1,\n                    0,\n                    \"V\",\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass ?: return@descCopy\n                val descViewHolderIndex = dexHelper.encodeClassIndex(descViewHolderClass)\n                val copyMethodIndex = dexHelper.findMethodUsingString(\n                    \"clipboard\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    descViewHolderIndex,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull() ?: return@descCopy\n                val toCopyMethodIndex = dexHelper.findMethodInvoked(\n                    copyMethodIndex,\n                    -1,\n                    3,\n                    \"VLZL\",\n                    descViewHolderIndex,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull() ?: return@descCopy\n                val clickableSpanIndex = dexHelper.encodeClassIndex(ClickableSpan::class.java)\n                dexHelper.findMethodInvoked(\n                    toCopyMethodIndex,\n                    -1,\n                    2,\n                    \"VLL\",\n                    -1,\n                    longArrayOf(viewIndex, clickableSpanIndex),\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)\n                }.forEach {\n                    classes += class_ { name = it.declaringClass.name }\n                    methods += method { name = it.name }\n                }\n            }\n            comment3Copy = comment3Copy {\n                classesList.filter {\n                    it.startsWith(\"com.bilibili.app.comment3.ui.holder.handle.CommentContentRichTextHandler\")\n                }.map { it.on(classloader) }.flatMap { c ->\n                    c.declaredMethods.filter {\n                        (it.isPrivate && it.parameterCount == 6 && it.parameterTypes[5] == View::class.java) ||\n                                (it.isPrivate && it.parameterCount == 3 && it.parameterTypes[2] == View::class.java)\n                    }\n                }.firstOrNull()?.let {\n                    Log.d(it.declaringClass.name + it.name)\n                    class_ = class_ { name = it.declaringClass.name }\n                    method = method { name = it.name }\n                    comment3ViewIndex = it.parameterCount - 1\n                }\n            }\n            dexHelper.findMethodUsingString(\n                \"BangumiAllButton\",\n                true, -1, 0, null, -1, null, null, null, true\n            ).firstOrNull()?.let {\n                dexHelper.decodeMethodIndex(it)\n            }?.declaringClass?.declaringClass?.let {\n                bangumiSeason = class_ { name = it.name }\n                bangumiSeasonActivityEntrance = method {\n                    name = it.declaredMethods.firstOrNull {\n                        it.parameterTypes.isEmpty() && it.genericReturnType.toString()\n                            .contains(\"ActivityEntrance\")\n                    }?.name ?: return@method\n                }\n            }\n            kanBan = kanBan {\n                val statusClass =\n                    \"tv.danmaku.bili.ui.kanban.KanBanUserStatus\".findClassOrNull(classloader)\n                        ?: return@kanBan\n                statusClass.runCatchingOrNull {\n                    getDeclaredMethod(\"isUseKanBan\")\n                }?.let {\n                    dexHelper.findMethodInvoked(\n                        dexHelper.encodeMethodIndex(it),\n                        -1,\n                        1,\n                        \"VL\",\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).firstOrNull()\n                }?.let {\n                    dexHelper.decodeMethodIndex(it)\n                }?.let {\n                    class_ = class_ { name = it.declaringClass.name }\n                    method = method { name = it.name }\n                }\n            }\n            toastHelper = toastHelper {\n                val showToastCallerIndex = dexHelper.findMethodUsingString(\n                    \"main.lessonmodel.enterdetail.change-pswd-success.click\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull() ?: return@toastHelper\n                val contextIndex = dexHelper.encodeClassIndex(Context::class.java)\n                val stringIndex = dexHelper.encodeClassIndex(String::class.java)\n                val showToastMethod = dexHelper.findMethodInvoking(\n                    showToastCallerIndex,\n                    -1,\n                    3,\n                    \"VLLI\",\n                    -1,\n                    longArrayOf(contextIndex, stringIndex, -1),\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@toastHelper\n                val toastHelperClass = showToastMethod.declaringClass\n                class_ = class_ { name = toastHelperClass.name }\n                show = method { name = showToastMethod.name }\n                cancel = method {\n                    name = toastHelperClass.declaredMethods.firstOrNull {\n                        it.isStatic && it.parameterTypes.isEmpty()\n                    }?.name ?: return@method\n                }\n            }\n            brotliInputStream = class_ {\n                name = dexHelper.findMethodUsingString(\n                    \"Brotli decoder initialization failed\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.declaringClass?.name ?: return@class_\n            }\n            dexHelper.findMethodUsingString(\n                \"player.player.story-button.0.player\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                false\n            ).asSequence().mapNotNull {\n                val clazz = dexHelper.decodeMethodIndex(it)?.declaringClass\n                val method = clazz?.declaredMethods?.find { m ->\n                    m.isStatic && m.parameterTypes.size == 1 && m.parameterTypes[0] == clazz && m.returnType == Boolean::class.javaPrimitiveType\n                }\n                if (clazz != null && method != null) {\n                    playerFullStoryWidget {\n                        class_ = class_ { name = clazz.name }\n                        this.method = method { name = method.name }\n                    }\n                } else null\n            }.let { playerFullStoryWidget.addAll(it.toList()) }\n            biliCall = biliCall {\n                val biliCallClass = \"com.bilibili.okretro.call.BiliCall\".from(classloader)\n                    ?: dexHelper.findMethodUsingString(\n                        \"Any arguments of BiliCall constructor can not be null\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }?.declaringClass ?: return@biliCall\n                val setParserMethod = biliCallClass.methods.find {\n                    it.parameterTypes.size == 1 && it.parameterTypes[0].let { c ->\n                        c.isInterface && c.interfaces.size == 1 && c.interfaces[0].declaredMethods.size == 1\n                    }\n                } ?: return@biliCall\n                val requestFiled = biliCallClass.declaredFields.find {\n                    it.type.name == this@hookInfo.okHttp.request.class_.name\n                } ?: return@biliCall\n                class_ = class_ { name = biliCallClass.name }\n                parser = class_ { name = setParserMethod.parameterTypes[0].name }\n                setParser = method { name = setParserMethod.name }\n                request = field { name = requestFiled.name }\n            }\n            biliConfig = biliConfig {\n                val biliConfigClass = \"com.bilibili.api.BiliConfig\".from(classloader)\n                    ?: dexHelper.findMethodUsingString(\n                        \"Call BiliConfig.init() first!\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).asSequence().firstNotNullOfOrNull {\n                        dexHelper.decodeMethodIndex(it)\n                    }?.declaringClass ?: return@biliConfig\n                class_ = class_ {\n                    name = biliConfigClass.name\n                }\n                val biliConfigClassIdx = dexHelper.encodeClassIndex(biliConfigClass)\n                val loginMethodIdx = dexHelper.findMethodUsingString(\n                    \"url_find_pwd_no_sms\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstOrNull() ?: return@biliConfig\n                dexHelper.findMethodInvoking(\n                    loginMethodIdx,\n                    -1,\n                    0,\n                    null,\n                    biliConfigClassIdx,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                }?.let {\n                    getAppKey = method { name = it.name }\n                }\n            }\n            dexHelper.findMethodUsingString(\n                \"im.chat-group.msg.repost.click\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                true\n            ).asSequence().firstNotNullOfOrNull {\n                dexHelper.decodeMethodIndex(it) as? Method\n            }?.let {\n                val getContentStringMethod = it.parameterTypes[1].declaredMethods.find { m ->\n                    m.returnType == String::class.java && m.parameterTypes.isEmpty()\n                } ?: return@let\n                onOperateClick = method { name = it.name }\n                getContentString = method { name = getContentStringMethod.name }\n            }\n            livePagerRecyclerView = class_ {\n                val liveVerticalPagerView =\n                    \"com.bilibili.bililive.room.ui.roomv3.vertical.widget.LiveVerticalPagerView\"\n                        .from(classloader) ?: return@class_\n                name = liveVerticalPagerView.declaredFields.find {\n                    View::class.java.isAssignableFrom(it.type)\n                }?.type?.name ?: return@class_\n            }\n            updateInfoSupplier = updateInfoSupplier {\n                val checkMethod = dexHelper.findMethodUsingString(\n                    \"Do sync http request.\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@updateInfoSupplier\n                class_ = class_ { name = checkMethod.declaringClass.name }\n                check = method { name = checkMethod.name }\n            }\n            playerPreloadHolder = playerPreloadHolder {\n                dexHelper.findMethodUsingString(\n                    \"preloadKey is null\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.run {\n                    val stringClassIndex = dexHelper.encodeClassIndex(String::class.java)\n                    dexHelper.findMethodInvoking(\n                        this,\n                        stringClassIndex,\n                        2,\n                        \"LLL\",\n                        -1,\n                        null,\n                        longArrayOf(stringClassIndex),\n                        null,\n                        true\n                    ).firstOrNull()?.let {\n                        dexHelper.decodeMethodIndex(it)\n                    }\n                }?.let {\n                    class_ = class_ { name = it.declaringClass.name }\n                    get = method { name = it.name }\n                    return@playerPreloadHolder\n                }\n                // 粉版 7.39.0+\n                val geminiPreloadClass = dexHelper.findMethodUsingString(\n                    \"PreloadData(type=\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass?.declaringClass\n                } ?: return@playerPreloadHolder\n                geminiPreloadClass.declaredMethods.firstOrNull {\n                    it.isPublic && it.parameterTypes.count() == 1\n                }?.let {\n                    class_ = class_ { name = it.declaringClass.name }\n                    get = method { name = it.name }\n                }\n            }\n            dexHelper.findMethodUsingString(\n                \"player.unite_login_qn\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                false\n            ).asSequence().mapNotNull {\n                dexHelper.decodeMethodIndex(it)?.let { m ->\n                    playerQualityService {\n                        class_ = class_ { name = m.declaringClass.name }\n                        getDefaultQnThumb = method { name = m.name }\n                    }\n                }\n            }.let { playerQualityService.addAll(it.toList()) }\n            playerSettingHelper = playerSettingHelper {\n                val getDefaultQnMethod = dexHelper.findMethodUsingString(\n                    \"quality settings:\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@playerSettingHelper\n                class_ = class_ { name = getDefaultQnMethod.declaringClass.name }\n                getDefaultQn = method { name = getDefaultQnMethod.name }\n            }\n            val autoSupremumQualityClass = dexHelper.findMethodUsingString(\n                \"AutoSupremumQuality(loginHalfScreen=\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                true\n            ).asSequence().firstNotNullOfOrNull {\n                dexHelper.decodeMethodIndex(it)\n            }?.declaringClass?.also {\n                autoSupremumQuality = autoSupremumQuality {\n                    class_ = class_ {\n                        name = it.name\n                    }\n                }\n            }\n            qualityStrategyProvider = qualityStrategyProvider {\n                val buildStrategyMethod = dexHelper.findMethodUsingString(\n                    \"Quality Strategy share:\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@qualityStrategyProvider\n                val providerClass = buildStrategyMethod.declaringClass\n                val selectQualityMethod = providerClass.declaredMethods.asSequence().filter {\n                    it.parameterCount == 3 && it.parameterTypes.contentEquals(\n                        arrayOf(\n                            autoSupremumQualityClass,\n                            Boolean::class.javaPrimitiveType,\n                            Boolean::class.javaPrimitiveType\n                        )\n                    )\n                }.firstNotNullOfOrNull { method ->\n                    val methodIdx = dexHelper.encodeMethodIndex(method)\n                    val isConnectedMethods = dexHelper.findMethodInvoking(\n                        methodIdx,\n                        dexHelper.encodeClassIndex(Int::class.javaPrimitiveType!!),\n                        1,\n                        null,\n                        -1,\n                        longArrayOf(dexHelper.encodeClassIndex(Context::class.java)),\n                        null,\n                        null,\n                        true\n                    )\n                    if (isConnectedMethods.isEmpty()) method else null\n                } ?: return@qualityStrategyProvider\n                class_ = class_ { name = providerClass.name }\n                selectQuality = method { name = selectQualityMethod.name }\n            }\n            liveRtcHelper = liveRtcHelper {\n                val liveRtcEnable = dexHelper.findMethodUsingString(\n                    \"systemSupportLiveP2P\",\n                    true,\n                    dexHelper.encodeClassIndex(Boolean::class.java),\n                    0,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@liveRtcHelper\n                liveRtcEnableClass = class_ { name = liveRtcEnable.declaringClass.name }\n                liveRtcEnableMethod = method { name = liveRtcEnable.name }\n            }\n            val themeColorsClass = dexHelper.findMethodUsingString(\n                \"GarbThemeColors(garb=\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                true\n            ).firstOrNull()?.let {\n                dexHelper.decodeMethodIndex(it)\n            }?.declaringClass?.also {\n                themeColors = class_ { name = it.name }\n            }\n            builtInThemes = builtInThemes {\n                val themeColorsConstIdx = themeColorsClass?.declaredConstructors?.maxByOrNull {\n                    it.parameterTypes.size\n                }?.let { dexHelper.encodeMethodIndex(it) } ?: return@builtInThemes\n                val clazz = dexHelper.findMethodInvoked(\n                    themeColorsConstIdx,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.run {\n                    dexHelper.findMethodInvoking(\n                        this,\n                        dexHelper.encodeClassIndex(Map::class.java),\n                        0,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        true\n                    ).firstOrNull()?.let {\n                        dexHelper.decodeMethodIndex(it)\n                    }\n                }?.declaringClass ?: return@builtInThemes\n                val field = clazz.declaredFields.firstOrNull {\n                    it.type == Map::class.java\n                } ?: return@builtInThemes\n                class_ = class_ { name = clazz.name }\n                all = field { name = field.name }\n            }\n            biliGlobalPreference = biliGlobalPreference {\n                val clazz = \"com.bilibili.base.BiliGlobalPreferenceHelper\".from(classloader)\n                    ?: dexHelper.findMethodUsingString(\n                        \"instance.bili_preference\",\n                        false,\n                        -1,\n                        -1,\n                        null,\n                        -1,\n                        null,\n                        null,\n                        null,\n                        false\n                    ).asSequence().firstNotNullOfOrNull { idx ->\n                        dexHelper.decodeMethodIndex(idx)?.declaringClass?.takeIf { !it.superclass.isAbstract }\n                    } ?: return@biliGlobalPreference\n                val method = clazz.declaredMethods.firstOrNull {\n                    it.isStatic && it.parameterTypes.isEmpty() && it.returnType == SharedPreferences::class.java\n                } ?: return@biliGlobalPreference\n                class_ = class_ { name = clazz.name }\n                get = method { name = method.name }\n            }\n            cardClickProcessor = cardClickProcessor {\n                val method = dexHelper.findMethodUsingString(\n                    \"action:feed:dislike_reason\",\n                    false,\n                    -1,\n                    5,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).firstOrNull()?.let {\n                    dexHelper.decodeMethodIndex(it)\n                } ?: return@cardClickProcessor\n                class_ = class_ { name = method.declaringClass.name }\n                onFeedClicked = method { name = method.name }\n            }\n            dexHelper.findMethodUsingString(\n                \"PublishToFollowingConfig(visible=\",\n                false,\n                -1,\n                -1,\n                null,\n                -1,\n                null,\n                null,\n                null,\n                true\n            ).firstOrNull()?.let {\n                dexHelper.decodeMethodIndex(it)?.declaringClass\n            }?.let {\n                publishToFollowingConfig = class_ { name = it.name }\n            }\n\n            continuation = continuation {\n                val continuationImpl = dexHelper.findMethodUsingString(\n                    \"create(Continuation) has not been overridden\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                } ?: return@continuation\n                val continuation = continuationImpl.interfaces.firstOrNull() ?: return@continuation\n                class_ = class_ { name = continuation.name }\n            }\n            vipQualityTrialService = vipQualityTrialService {\n                val serviceClass = dexHelper.findMethodUsingString(\n                    \"go to buy vip: \",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                }.firstOrNull {\n                    it.name.startsWith(\"com.bilibili\")\n                } ?: return@vipQualityTrialService\n                val canTrialMethod = serviceClass.declaredMethods.asSequence().filter {\n                    it.returnType == Boolean::class.javaPrimitiveType && it.parameterCount == 0\n                }.firstNotNullOfOrNull { candidate ->\n                    val getTrialInfoMethods = dexHelper.findMethodInvoking(\n                        dexHelper.encodeMethodIndex(candidate),\n                        -1,\n                        0,\n                        null,\n                        dexHelper.encodeClassIndex(serviceClass),\n                        null,\n                        null,\n                        null,\n                        true\n                    )\n                    if (getTrialInfoMethods.isNotEmpty()) candidate else null\n                } ?: return@vipQualityTrialService\n                class_ = class_ { name = serviceClass.name }\n                canTrial = method { name = canTrialMethod.name }\n            }\n            liveQuality = liveQuality {\n                val utilClass = dexHelper.findMethodUsingString(\n                    \"select 秒开 play url --codec：\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).map {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                }.firstOrNull() ?: return@liveQuality\n                val selectorDataClass = dexHelper.findMethodUsingString(\n                    \"LiveUrlSelectorData(playUrl=\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).map {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                }.firstOrNull() ?: return@liveQuality\n                val buildSelectorDataMethod = utilClass.declaredMethods.firstOrNull {\n                    it.returnType == selectorDataClass && it.parameterCount == 1 && it.parameterTypes[0] == Uri::class.java\n                } ?: return@liveQuality\n                val switchAutoMethod = dexHelper.findMethodUsingString(\n                    \"switchAuto \",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).map {\n                    dexHelper.decodeMethodIndex(it)\n                }.firstOrNull() ?: return@liveQuality\n                val interceptorClass = dexHelper.findMethodUsingString(\n                    \"inject common param to body failure : \",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).map {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                }.firstOrNull() ?: return@liveQuality\n                val interceptMethod = interceptorClass.declaredMethods.firstOrNull {\n                    it.isPublic && it.parameterCount == 1 && it.returnType == it.parameterTypes[0]\n                } ?: return@liveQuality\n                selectUtil = livePlayUrlSelectUtil {\n                    class_ = class_ { name = utilClass.name }\n                    buildSelectorData = method { name = buildSelectorDataMethod.name }\n                }\n                sourceService = liveRTCSourceServiceImpl {\n                    class_ = class_ { name = switchAutoMethod.declaringClass.name }\n                    switchAuto = method { name = switchAutoMethod.name }\n                }\n                interceptor = defaultRequestIntercept {\n                    class_ = class_ { name = interceptorClass.name }\n                    intercept = method { name = interceptMethod.name }\n                }\n            }\n            preBuiltConfig = preBuiltConfig {\n                val getMap = dexHelper.findMethodUsingString(\n                    \"/config.json\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().mapNotNull { dexHelper.decodeMethodIndex(it) }.firstOrNull()\n                    ?: return@preBuiltConfig\n                class_ = class_ { name = getMap.declaringClass.name }\n                get = method { name = getMap.name }\n            }\n            dataSP = dataSP {\n                val spxClass =\n                    (\"com.bilibili.lib.blkv.SharedPrefX\" from classloader) ?: return@dataSP\n                val blkvClass = (\"com.bilibili.lib.blkv.BLKV\" from classloader) ?: return@dataSP\n                val getBLSP = runCatchingOrNull {\n                    blkvClass.getDeclaredMethod(\n                        \"getBLSharedPreferences\",\n                        Context::class.java,\n                        File::class.java,\n                        Boolean::class.javaPrimitiveType,\n                        Int::class.javaPrimitiveType\n                    )\n                } ?: return@dataSP\n                val getDataSP = dexHelper.findMethodInvoked(\n                    dexHelper.encodeMethodIndex(getBLSP),\n                    dexHelper.encodeClassIndex(spxClass),\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    false\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)\n                }.firstOrNull {\n                    Log.d(it)\n                    it.declaringClass.name.startsWith(\"com.bilibili.lib.blconfig.internal.TypedContext\")\n                } ?: return@dataSP\n                class_ = class_ { name = getDataSP.declaringClass.name }\n                get = method { name = getDataSP.name }\n            }\n            pegasusParser = class_ {\n                val getPegasusParser = dexHelper.findMethodUsingString(\n                    \"[Pegasus]PegasusParser\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().mapNotNull { dexHelper.decodeMethodIndex(it) }.firstOrNull()\n                    ?: return@class_\n                name = getPegasusParser.declaringClass.name\n            }\n            resolveClientCompanion = resolveClientCompanion {\n                val resolveClientClass = dexHelper.findMethodUsingString(\n                    \"Invalid segment id: %s, segment list size:%s\",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    -1,\n                    null,\n                    null,\n                    null,\n                    true\n                ).asSequence().firstNotNullOfOrNull {\n                    dexHelper.decodeMethodIndex(it)?.declaringClass\n                } ?: return@resolveClientCompanion\n                val resolveClientCompanionClass = \"${resolveClientClass.name}\\$a\".from(classloader)\n                    ?: return@resolveClientCompanion\n                val paramsClass =\n                    \"com.bilibili.app.gemini.base.player.GeminiCommonResolverParams\".from(\n                        classloader\n                    )\n                val videoDownloadEntryClass =\n                    \"com.bilibili.videodownloader.model.VideoDownloadEntry\".from(classloader)\n                val buildParamsMethod = resolveClientCompanionClass.declaredMethods.firstOrNull {\n                    it.returnType == paramsClass && it.parameterCount == 1 && it.parameterTypes[0] == videoDownloadEntryClass\n                } ?: return@resolveClientCompanion\n                class_ = class_ { name = resolveClientCompanionClass.name }\n                buildCommonResolverParams = method { name = buildParamsMethod.name }\n            }\n            gCommonResolverParams = gCommonResolverParams {\n                val commonResolverParamsClass =\n                    \"com.bilibili.app.gemini.base.player.GeminiCommonResolverParams\".from(\n                        classloader\n                    ) ?: return@gCommonResolverParams\n                val setExtraContentMethod = commonResolverParamsClass.declaredMethods?.firstOrNull {\n                    it.returnType == Void.TYPE && it.parameterCount == 1 && it.parameterTypes[0] == Map::class.java\n                } ?: return@gCommonResolverParams\n                class_ = class_ { name = commonResolverParamsClass.name }\n                setExtraContent = method { name = setExtraContentMethod.name }\n            }\n            rewardAd = rewardAd {\n                val rewardAdActivityClass =\n                    \"com.bilibili.ad.reward.RewardAdActivity\".from(classloader) ?: return@rewardAd\n\n                class_ = class_ { name = rewardAdActivityClass.name }\n                val rewardFlagField =\n                    rewardAdActivityClass.declaredFields.filter { it.type == Boolean::class.javaPrimitiveType }\n                        .getOrNull(1) ?: return@rewardAd\n\n                rewardFlag = field { name = rewardFlagField.name }\n\n            }\n            storyPagerPlayer = storyPagerPlayer {\n                val storyPagerPlayerClass =\n                    \"com.bilibili.video.story.player.StoryPagerPlayer\".from(classloader) ?: return@storyPagerPlayer\n\n                class_ = class_ { name = storyPagerPlayerClass.name }\n\n                val addVideoMethod = dexHelper.findMethodUsingString(\n                    \" add \",\n                    false,\n                    -1,\n                    -1,\n                    null,\n                    dexHelper.encodeClassIndex(storyPagerPlayerClass),\n                    null,\n                    longArrayOf(dexHelper.encodeClassIndex(List::class.java)),\n                    null,\n                    true\n                ).asSequence().mapNotNull {\n                    dexHelper.decodeMethodIndex(it)\n                }.firstOrNull() ?: return@storyPagerPlayer\n\n                addVideo = method { name = addVideoMethod.name }\n            }\n\n            dexHelper.close()\n        }\n\n        @Volatile\n        lateinit var instance: BiliBiliPackage\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/ColorChooseDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.AlertDialog\nimport android.content.Context\nimport android.graphics.Color\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.view.View\nimport android.widget.EditText\nimport android.widget.SeekBar\nimport android.widget.SeekBar.OnSeekBarChangeListener\nimport android.widget.TextView\nimport me.iacn.biliroaming.utils.inflateLayout\n\n/**\n * Created by iAcn on 2019/7/14\n * Email i@iacn.me\n */\nclass ColorChooseDialog(context: Context, defColor: Int) : AlertDialog.Builder(context) {\n    private val view = context.inflateLayout(R.layout.dialog_color_choose)\n    private val sampleView: View = view.findViewById(R.id.view_sample)\n    private val etColor: EditText = view.findViewById(R.id.et_color)\n    private val sbColorR: SeekBar = view.findViewById(R.id.sb_colorR)\n    private val sbColorG: SeekBar = view.findViewById(R.id.sb_colorG)\n    private val sbColorB: SeekBar = view.findViewById(R.id.sb_colorB)\n    private val tvColorR: TextView = view.findViewById(R.id.tv_colorR)\n    private val tvColorG: TextView = view.findViewById(R.id.tv_colorG)\n    private val tvColorB: TextView = view.findViewById(R.id.tv_colorB)\n    val color: Int\n        get() = Color.rgb(sbColorR.progress, sbColorG.progress, sbColorB.progress)\n\n    private fun setEditTextListener() {\n        etColor.addTextChangedListener(object : TextWatcher {\n            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}\n            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {\n                updateValue(handleUnknownColor(s.toString()))\n            }\n\n            override fun afterTextChanged(s: Editable) {}\n        })\n    }\n\n    private fun setSeekBarListener() {\n        val listener: OnSeekBarChangeListener = object : OnSeekBarChangeListener {\n            override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {\n                if (fromUser) {\n                    val color = Color.rgb(sbColorR.progress, sbColorG.progress, sbColorB.progress)\n                    etColor.setText(String.format(\"%06X\", 0xFFFFFF and color))\n                }\n                tvColorR.text = sbColorR.progress.toString()\n                tvColorG.text = sbColorG.progress.toString()\n                tvColorB.text = sbColorB.progress.toString()\n            }\n\n            override fun onStartTrackingTouch(seekBar: SeekBar) {}\n            override fun onStopTrackingTouch(seekBar: SeekBar) {}\n        }\n        sbColorR.setOnSeekBarChangeListener(listener)\n        sbColorG.setOnSeekBarChangeListener(listener)\n        sbColorB.setOnSeekBarChangeListener(listener)\n    }\n\n    private fun updateValue(color: Int) {\n        sampleView.setBackgroundColor(color)\n        val progressR = Color.red(color)\n        val progressG = Color.green(color)\n        val progressB = Color.blue(color)\n        sbColorR.progress = progressR\n        sbColorG.progress = progressG\n        sbColorB.progress = progressB\n        tvColorR.text = progressR.toString()\n        tvColorG.text = progressG.toString()\n        tvColorB.text = progressB.toString()\n    }\n\n    private fun handleUnknownColor(color: String) =\n        try {\n            Color.parseColor(\"#$color\")\n        } catch (e: IllegalArgumentException) {\n            Color.BLACK\n        }\n\n    init {\n        setView(view)\n        setEditTextListener()\n        setSeekBarListener()\n        updateValue(defColor)\n        etColor.setText(String.format(\"%06X\", 0xFFFFFF and defColor))\n        setTitle(\"自选颜色\")\n        setNegativeButton(\"取消\", null)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/CommentFilterDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.content.SharedPreferences\nimport android.view.inputmethod.EditorInfo\nimport android.widget.*\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.dp\nimport me.iacn.biliroaming.utils.inflateLayout\n\nclass CommentFilterDialog(activity: Activity, prefs: SharedPreferences) :\n    BaseWidgetDialog(activity) {\n    init {\n        val scrollView = ScrollView(context).apply {\n            scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY\n        }\n        val root = LinearLayout(context).apply {\n            orientation = LinearLayout.VERTICAL\n            layoutParams = FrameLayout.LayoutParams(\n                FrameLayout.LayoutParams.MATCH_PARENT,\n                FrameLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        scrollView.addView(root)\n\n        val (contentGroup, contentRegexSwitch) = root.addKeywordGroup(\n            string(R.string.keyword_group_name_content), 40.dp, true\n        )\n        contentRegexSwitch.isChecked = prefs.getBoolean(\"comment_filter_content_regex_mode\", false)\n        val upNameGroup = root.addKeywordGroup(string(R.string.keyword_group_name_at_up), 40.dp).first\n        val uidGroup = root.addKeywordGroup(\n            string(R.string.keyword_group_name_at_uid),\n            40.dp,\n            inputType = EditorInfo.TYPE_CLASS_NUMBER\n        ).first\n        prefs.getStringSet(\"comment_filter_keyword_content\", null)?.forEach {\n            contentGroup.addView(keywordInputItem(contentGroup, it).first)\n        }\n        prefs.getStringSet(\"comment_filter_keyword_at_upname\", null)?.forEach {\n            upNameGroup.addView(keywordInputItem(upNameGroup, it).first)\n        }\n        prefs.getStringSet(\"comment_filter_keyword_at_uid\", null)?.forEach {\n            uidGroup.addView(keywordInputItem(uidGroup, it, EditorInfo.TYPE_CLASS_NUMBER).first)\n        }\n\n        val blockAtCommentSwitch = switchPrefsItem(\n            string(R.string.comment_filter_block_at_comment_title)\n        ).let { root.addView(it.first); it.second }\n        blockAtCommentSwitch.isChecked = prefs.getBoolean(\"comment_filter_block_at_comment\", false)\n\n        val targetCommentAuthorLevelTitle =\n            categoryTitle(string(R.string.target_comment_author_level_title))\n        root.addView(targetCommentAuthorLevelTitle)\n\n        val seekBarView = context.inflateLayout(R.layout.seekbar_dialog)\n        val currentTargetCommentAuthorLevel =\n            prefs.getLong(\"target_comment_author_level\", 0L).toInt()\n        val tvHint = seekBarView.findViewById<TextView>(R.id.tvHint).apply {\n            text = if (currentTargetCommentAuthorLevel == 0) \"关闭\" else context.getString(\n                R.string.danmaku_filter_weight_hint,\n                currentTargetCommentAuthorLevel\n            )\n        }\n        val seekBar = seekBarView.findViewById<SeekBar>(R.id.seekBar).apply {\n            max = 6\n            setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {\n                override fun onProgressChanged(\n                    seekBar: SeekBar?, progress: Int, fromUser: Boolean\n                ) {\n                    tvHint.text =\n                        if (progress == 0) \"关闭\" else context.getString(\n                            R.string.danmaku_filter_weight_hint,\n                            progress\n                        )\n                }\n\n                override fun onStartTrackingTouch(seekBar: SeekBar?) {}\n                override fun onStopTrackingTouch(seekBar: SeekBar?) {}\n            })\n            progress = currentTargetCommentAuthorLevel\n        }\n        root.addView(seekBarView)\n\n        setTitle(string(R.string.filter_comment_title))\n\n        setPositiveButton(android.R.string.ok) { _, _ ->\n\n            val contents = contentGroup.getKeywords()\n            val contentRegexMode = contentRegexSwitch.isChecked\n            if (contentRegexMode && contents.runCatching { forEach { it.toRegex() } }.isFailure) {\n                Log.toast(string(R.string.invalid_regex), force = true)\n                return@setPositiveButton\n            }\n\n            prefs.edit().apply {\n                putStringSet(\"comment_filter_keyword_content\", contents)\n                putStringSet(\"comment_filter_keyword_at_upname\", upNameGroup.getKeywords())\n                putStringSet(\"comment_filter_keyword_at_uid\", uidGroup.getKeywords())\n                putBoolean(\"comment_filter_content_regex_mode\", contentRegexMode)\n                putBoolean(\"comment_filter_block_at_comment\", blockAtCommentSwitch.isChecked)\n                putLong(\"target_comment_author_level\", seekBar.progress.toLong())\n            }.apply()\n            Log.toast(string(R.string.prefs_save_success_and_reboot))\n        }\n        setNegativeButton(android.R.string.cancel, null)\n\n        root.setPadding(16.dp, 10.dp, 16.dp, 10.dp)\n\n        setView(scrollView)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/Constant.kt",
    "content": "package me.iacn.biliroaming\n\n/**\n * Created by iAcn on 2019/4/12\n * Email i@iacn.me\n */\nobject Constant {\n    const val PINK_PACKAGE_NAME = \"tv.danmaku.bili\"\n    const val BLUE_PACKAGE_NAME = \"com.bilibili.app.blue\"\n    const val PLAY_PACKAGE_NAME = \"com.bilibili.app.in\"\n    const val HD_PACKAGE_NAME = \"tv.danmaku.bilibilihd\"\n    val BILIBILI_PACKAGE_NAME = hashMapOf(\n        \"原版\" to PINK_PACKAGE_NAME,\n        \"概念版\" to BLUE_PACKAGE_NAME,\n        \"play版\" to PLAY_PACKAGE_NAME,\n        \"HD版\" to HD_PACKAGE_NAME\n    )\n    const val TAG = \"BiliRoaming\"\n    const val HOOK_INFO_FILE_NAME = \"hookinfo.pb\"\n    const val TYPE_SEASON_ID = 0\n    const val TYPE_MEDIA_ID = 1\n    const val TYPE_EPISODE_ID = 2\n    const val CUSTOM_COLOR_KEY = \"biliroaming_custom_color\"\n    const val CURRENT_COLOR_KEY = \"theme_entries_current_key\"\n    const val DEFAULT_CUSTOM_COLOR = -0xe6b7d\n    const val zoneUrl = \"https://api.bilibili.com/x/web-interface/zone\"\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/CustomSubtitleDialog.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.app.Fragment\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.content.SharedPreferences\nimport android.graphics.Color\nimport android.text.SpannableString\nimport android.text.Spanned\nimport android.view.View\nimport android.widget.*\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.hook.SubtitleHook\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.inflateLayout\n\nclass CustomSubtitleDialog(activity: Activity, fragment: Fragment, prefs: SharedPreferences) :\n    AlertDialog.Builder(activity) {\n    private var fontStatus: TextView? = null\n\n    init {\n        val oldClient = instance.cronCanvasClass == null\n        val view = context.inflateLayout(R.layout.custom_subtitle_dialog)\n        val noBgSwitch = view.findViewById<Switch>(R.id.noBg).apply {\n            isSoundEffectsEnabled = false\n            isHapticFeedbackEnabled = false\n            isChecked = prefs.getBoolean(tag.toString(), true)\n        }\n        val llNoBg = view.findViewById<View>(R.id.ll_noBg)\n        llNoBg.setOnClickListener { noBgSwitch.toggle() }\n        val boldSwitch = view.findViewById<Switch>(R.id.bold).apply {\n            isSoundEffectsEnabled = false\n            isHapticFeedbackEnabled = false\n            isChecked = prefs.getBoolean(tag.toString(), true)\n        }\n        val llBold = view.findViewById<View>(R.id.ll_bold)\n        llBold.setOnClickListener { boldSwitch.toggle() }\n        fontStatus = view.findViewById(R.id.tv_fontStatus)\n        refreshFontStatus()\n        val fontColor = view.findViewById<EditText>(R.id.font_color)\n        fontColor.setText(prefs.getString(fontColor.tag.toString(), \"FFFFFFFF\"))\n        val backgroundColor = view.findViewById<EditText>(R.id.background_color)\n        backgroundColor.setText(prefs.getString(backgroundColor.tag.toString(), \"20000000\"))\n        val fontSize = view.findViewById<EditText>(R.id.font_size)\n        fontSize.setText(prefs.getInt(fontSize.tag.toString(), 30).toString())\n        val fontSizePortrait = view.findViewById<EditText>(R.id.fontSizePortrait)\n        fontSizePortrait.setText(prefs.getInt(fontSizePortrait.tag.toString(), 0).toString())\n        val fontSizeLandscape = view.findViewById<EditText>(R.id.fontSizeLandscape)\n        fontSizeLandscape.setText(prefs.getInt(fontSizeLandscape.tag.toString(), 0).toString())\n        val fontBlurSolid = view.findViewById<EditText>(R.id.font_blur_solid)\n        fontBlurSolid.setText(prefs.getInt(fontBlurSolid.tag.toString(), 1).toString())\n        val strokeColor = view.findViewById<EditText>(R.id.stroke_color)\n        strokeColor.setText(prefs.getString(strokeColor.tag.toString(), \"FF000000\"))\n        val strokeWidth = view.findViewById<EditText>(R.id.stroke_width)\n        strokeWidth.setText(prefs.getFloat(strokeWidth.tag.toString(), 5.0F).toString())\n        val fixBreak = view.findViewById<CheckBox>(R.id.cb_fixBreak)\n        fixBreak.isChecked = prefs.getBoolean(fixBreak.tag.toString(), false)\n        val offset = view.findViewById<EditText>(R.id.subOffset)\n        offset.setText(prefs.getInt(offset.tag.toString(), 0).toString())\n        view.findViewById<Button>(R.id.btn_pv).setOnClickListener {\n            val testText = view.findViewById<EditText>(R.id.et_testText).text.toString()\n            val spannableString = SpannableString(testText)\n            val fc = fontColor.text.toString()\n            val bc = backgroundColor.text.toString()\n            val fs = fontSize.text.toString().toInt()\n            val fbs = fontBlurSolid.text.toString().toInt()\n            val sc = strokeColor.text.toString()\n            val sw = strokeWidth.text.toString().toFloat()\n            SubtitleHook.subtitleStylizeRunner(\n                spannableString,\n                0,\n                spannableString.length,\n                Spanned.SPAN_INCLUSIVE_EXCLUSIVE,\n                fbs, fc, fs, bc, sc, sw,\n                fixBreak.isChecked\n            )\n            view.findViewById<TextView>(R.id.tv_pvBlack).text = spannableString\n            view.findViewById<TextView>(R.id.tv_pvWhite).text = spannableString\n            view.findViewById<TextView>(R.id.tv_pvTp).text = spannableString\n        }\n        view.findViewById<Button>(R.id.btn_chooseColorBc).setOnClickListener {\n            ARGBColorChooseDialog(context, Color.parseColor(\"#${backgroundColor.text}\")).apply {\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    backgroundColor.setText(String.format(\"%08X\", 0xFFFFFFFF.toInt() and color))\n                }\n            }.show()\n        }\n        view.findViewById<Button>(R.id.btn_chooseColorFc).setOnClickListener {\n            ARGBColorChooseDialog(context, Color.parseColor(\"#${fontColor.text}\")).apply {\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    fontColor.setText(String.format(\"%08X\", 0xFFFFFFFF.toInt() and color))\n                }\n            }.show()\n        }\n        view.findViewById<Button>(R.id.btn_chooseColorSc).setOnClickListener {\n            ARGBColorChooseDialog(context, Color.parseColor(\"#${strokeColor.text}\")).apply {\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    strokeColor.setText(String.format(\"%08X\", 0xFFFFFFFF.toInt() and color))\n                }\n            }.show()\n        }\n        view.findViewById<Button>(R.id.btn_importFont).setOnClickListener {\n            try {\n                fragment.startActivityForResult(\n                    Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {\n                        type = \"font/*\"\n                        addCategory(Intent.CATEGORY_OPENABLE)\n                    }, \"选择字体文件\"),\n                    2338\n                )\n            } catch (ex: ActivityNotFoundException) {\n                Log.toast(\"请安装文件管理器\")\n            }\n        }\n        view.findViewById<Button>(R.id.btn_resetFont).setOnClickListener {\n            SubtitleHook.fontFile.delete()\n            refreshFontStatus()\n        }\n\n        if (oldClient) {\n            llNoBg.visibility = View.GONE\n            llBold.visibility = View.GONE\n            view.findViewById<View>(R.id.ll_font).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_sizePortrait).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_sizeLandscape).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_subOffset).visibility = View.GONE\n        } else {\n            view.findViewById<View>(R.id.pvBlack).visibility = View.GONE\n            view.findViewById<View>(R.id.pvWhite).visibility = View.GONE\n            view.findViewById<View>(R.id.pvTp).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_bg).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_size).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_blur).visibility = View.GONE\n            view.findViewById<View>(R.id.ll_pv).visibility = View.GONE\n            fixBreak.visibility = View.GONE\n        }\n\n        setPositiveButton(android.R.string.ok) { _, _ ->\n            prefs.edit().apply {\n                putBoolean(noBgSwitch.tag.toString(), noBgSwitch.isChecked)\n                putBoolean(boldSwitch.tag.toString(), boldSwitch.isChecked)\n                putString(fontColor.tag.toString(), fontColor.text.toString())\n                putString(backgroundColor.tag.toString(), backgroundColor.text.toString())\n                putInt(fontSize.tag.toString(), fontSize.text.toString().toIntOrNull() ?: 30)\n                putInt(\n                    fontSizePortrait.tag.toString(),\n                    fontSizePortrait.text.toString().toIntOrNull() ?: 0\n                )\n                putInt(\n                    fontSizeLandscape.tag.toString(),\n                    fontSizeLandscape.text.toString().toIntOrNull() ?: 0\n                )\n                putInt(\n                    fontBlurSolid.tag.toString(),\n                    fontBlurSolid.text.toString().toIntOrNull() ?: 1\n                )\n                putString(strokeColor.tag.toString(), strokeColor.text.toString())\n                putFloat(\n                    strokeWidth.tag.toString(),\n                    strokeWidth.text.toString().toFloatOrNull() ?: 5.0F\n                )\n                putBoolean(fixBreak.tag.toString(), fixBreak.isChecked)\n                putInt(offset.tag.toString(), offset.text.toString().toIntOrNull() ?: 0)\n            }.apply()\n        }\n\n        setTitle(activity.getString(R.string.custom_subtitle_title))\n\n        setView(view)\n    }\n\n    private fun refreshFontStatus() {\n        fontStatus?.text = if (SubtitleHook.fontFile.isFile)\n            context.getString(R.string.custom_subtitle_status_custom)\n        else\n            context.getString(R.string.custom_subtitle_status_default)\n    }\n\n    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        val uri = data?.data\n        if (requestCode != 2338 || resultCode != Activity.RESULT_OK || uri == null) return\n        context.contentResolver.openInputStream(uri)?.use {\n            val fontFile = SubtitleHook.fontFile.apply { delete() }\n            it.copyTo(fontFile.outputStream())\n            refreshFontStatus()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/DynamicFilterDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.content.SharedPreferences\nimport android.view.inputmethod.EditorInfo\nimport android.widget.*\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.dp\n\nclass DynamicFilterDialog(activity: Activity, prefs: SharedPreferences) :\n    BaseWidgetDialog(activity) {\n    init {\n        val scrollView = ScrollView(context).apply {\n            scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY\n        }\n        val root = LinearLayout(context).apply {\n            orientation = LinearLayout.VERTICAL\n            layoutParams = FrameLayout.LayoutParams(\n                FrameLayout.LayoutParams.MATCH_PARENT,\n                FrameLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        scrollView.addView(root)\n\n        val preferVideoTabSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_prefer_video_tab)\n        ).let { root.addView(it.first); it.second }\n        preferVideoTabSwitch.isChecked = prefs.getBoolean(\"prefer_video_tab\", false)\n\n        val rmCityTabSwitch = switchPrefsItem(string(R.string.purify_city_title))\n            .let { root.addView(it.first); it.second }\n        rmCityTabSwitch.isChecked = prefs.getBoolean(\"purify_city\", false)\n\n        val rmCampusTabSwitch = switchPrefsItem(string(R.string.purify_campus_title))\n            .let { root.addView(it.first); it.second }\n        rmCampusTabSwitch.isChecked = prefs.getBoolean(\"purify_campus\", false)\n\n        val rmTopicOfAllSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_all_rm_topic_title)\n        ).let { root.addView(it.first); it.second }\n        rmTopicOfAllSwitch.isChecked = prefs.getBoolean(\"customize_dynamic_all_rm_topic\", false)\n\n        val rmUpOfAllSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_all_rm_up_title)\n        ).let { root.addView(it.first); it.second }\n        rmUpOfAllSwitch.isChecked = prefs.getBoolean(\"customize_dynamic_all_rm_up\", false)\n\n        val rmLiveOfAllSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_all_rm_live_title)\n        ).let { root.addView(it.first); it.second }\n        rmLiveOfAllSwitch.isChecked = prefs.getBoolean(\"customize_dynamic_all_rm_live\", false)\n\n        val rmUpOfVideoSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_video_rm_up_title)\n        ).let { root.addView(it.first); it.second }\n        rmUpOfVideoSwitch.isChecked = prefs.getBoolean(\"customize_dynamic_video_rm_up\", false)\n\n        val filterApplyToVideoSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_filter_apply_to_video)\n        ).let { root.addView(it.first); it.second }\n        filterApplyToVideoSwitch.isChecked = prefs.getBoolean(\"filter_apply_to_video\", false)\n\n        val rmBlockedSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_rm_blocked_title)\n        ).let { root.addView(it.first); it.second }\n        rmBlockedSwitch.isChecked = prefs.getBoolean(\"customize_dynamic_rm_blocked\", false)\n\n        val rmAdLinkSwitch = switchPrefsItem(\n            string(R.string.customize_dynamic_rm_ad_link_title)\n        ).let { root.addView(it.first); it.second }\n        rmAdLinkSwitch.isChecked = prefs.getBoolean(\"customize_dynamic_rm_ad_link\", false)\n\n        val byTypeTitle = categoryTitle(string(R.string.customize_dynamic_by_type))\n        root.addView(byTypeTitle)\n\n        val gridLayout = GridLayout(context).apply {\n            columnCount = 4\n            layoutParams = LinearLayout.LayoutParams(\n                LinearLayout.LayoutParams.MATCH_PARENT,\n                LinearLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        root.addView(gridLayout)\n        val dynamicTypes = context.resources.getStringArray(R.array.dynamic_entries).zip(\n            context.resources.getStringArray(R.array.dynamic_values)\n        )\n        val colSpec = fun(colWeight: Float) = GridLayout.spec(GridLayout.UNDEFINED, colWeight)\n        val rowSpec = { GridLayout.spec(GridLayout.UNDEFINED) }\n        val typePrefs = prefs.getStringSet(\"customize_dynamic_type\", null) ?: setOf()\n        dynamicTypes.forEach { item ->\n            val checkBox = CheckBox(context).apply {\n                tag = item.second\n                layoutParams = GridLayout.LayoutParams(rowSpec(), colSpec(1F))\n                isChecked = typePrefs.contains(item.second)\n            }\n            val textView = TextView(context).apply {\n                text = item.first\n                layoutParams = GridLayout.LayoutParams(rowSpec(), colSpec(2F))\n                setSingleLine()\n            }\n            gridLayout.addView(checkBox)\n            gridLayout.addView(textView)\n        }\n\n        val byKeywordTitle = categoryTitle(string(R.string.customize_dynamic_by_keyword))\n        root.addView(byKeywordTitle)\n\n        val (contentGroup, contentRegexSwitch) = root.addKeywordGroup(\n            string(R.string.keyword_group_name_content), 40.dp, true\n        )\n        contentRegexSwitch.isChecked = prefs.getBoolean(\"dynamic_content_regex_mode\", false)\n        val upNameGroup = root.addKeywordGroup(string(R.string.keyword_group_name_up), 40.dp).first\n        val uidGroup = root.addKeywordGroup(\n            string(R.string.keyword_group_name_uid),\n            40.dp,\n            inputType = EditorInfo.TYPE_CLASS_NUMBER\n        ).first\n        prefs.getStringSet(\"customize_dynamic_keyword_content\", null)?.forEach {\n            contentGroup.addView(keywordInputItem(contentGroup, it).first)\n        }\n        prefs.getStringSet(\"customize_dynamic_keyword_upname\", null)?.forEach {\n            upNameGroup.addView(keywordInputItem(upNameGroup, it).first)\n        }\n        prefs.getStringSet(\"customize_dynamic_keyword_uid\", null)?.forEach {\n            uidGroup.addView(keywordInputItem(uidGroup, it, EditorInfo.TYPE_CLASS_NUMBER).first)\n        }\n\n        setTitle(string(R.string.customize_dynamic_title))\n\n        setPositiveButton(android.R.string.ok) { _, _ ->\n            val typeValues = buildSet {\n                for (i in 0 until gridLayout.childCount step 2) {\n                    val view = gridLayout.getChildAt(i) as CheckBox\n                    if (view.isChecked) add(view.tag.toString())\n                }\n            }\n\n            val contents = contentGroup.getKeywords()\n            val contentRegexMode = contentRegexSwitch.isChecked\n            if (contentRegexMode && contents.runCatching { forEach { it.toRegex() } }.isFailure) {\n                Log.toast(string(R.string.invalid_regex), force = true)\n                return@setPositiveButton\n            }\n\n            prefs.edit().apply {\n                putBoolean(\"prefer_video_tab\", preferVideoTabSwitch.isChecked)\n                putBoolean(\"purify_city\", rmCityTabSwitch.isChecked)\n                putBoolean(\"purify_campus\", rmCampusTabSwitch.isChecked)\n                putBoolean(\"customize_dynamic_all_rm_topic\", rmTopicOfAllSwitch.isChecked)\n                putBoolean(\"customize_dynamic_all_rm_up\", rmUpOfAllSwitch.isChecked)\n                putBoolean(\"customize_dynamic_all_rm_live\", rmLiveOfAllSwitch.isChecked)\n                putBoolean(\"customize_dynamic_video_rm_up\", rmUpOfVideoSwitch.isChecked)\n                putBoolean(\"filter_apply_to_video\", filterApplyToVideoSwitch.isChecked)\n                putBoolean(\"customize_dynamic_rm_blocked\", rmBlockedSwitch.isChecked)\n                putBoolean(\"customize_dynamic_rm_ad_link\", rmAdLinkSwitch.isChecked)\n                putStringSet(\"customize_dynamic_type\", typeValues)\n                putStringSet(\"customize_dynamic_keyword_content\", contents)\n                putStringSet(\"customize_dynamic_keyword_upname\", upNameGroup.getKeywords())\n                putStringSet(\"customize_dynamic_keyword_uid\", uidGroup.getKeywords())\n                putBoolean(\"dynamic_content_regex_mode\", contentRegexMode)\n            }.apply()\n            Log.toast(string(R.string.prefs_save_success_and_reboot))\n        }\n        setNegativeButton(android.R.string.cancel, null)\n\n        root.setPadding(16.dp, 10.dp, 16.dp, 10.dp)\n\n        setView(scrollView)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/HomeFilterDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.content.SharedPreferences\nimport android.view.inputmethod.EditorInfo\nimport android.widget.*\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.dp\nimport me.iacn.biliroaming.utils.migrateHomeFilterPrefsIfNeeded\n\nclass HomeFilterDialog(activity: Activity, prefs: SharedPreferences) :\n    BaseWidgetDialog(activity) {\n    init {\n        migrateHomeFilterPrefsIfNeeded()\n        val scrollView = ScrollView(context).apply {\n            scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY\n        }\n        val root = LinearLayout(context).apply {\n            orientation = LinearLayout.VERTICAL\n            layoutParams = FrameLayout.LayoutParams(\n                FrameLayout.LayoutParams.MATCH_PARENT,\n                FrameLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        scrollView.addView(root)\n        val hideTopSwitch = switchPrefsItem((string(R.string.hide_top_entrance_popular_summary)))\n            .let { root.addView(it.first); it.second }\n        hideTopSwitch.isChecked = prefs.getBoolean(\"hide_top_entrance_popular\", false)\n        val hideFollowSwitch = switchPrefsItem((string(R.string.hide_suggest_follow_popular_summary)))\n            .let { root.addView(it.first); it.second }\n        hideFollowSwitch.isChecked = prefs.getBoolean(\"hide_suggest_follow_popular\", false)\n\n        val hideTopicListSwitch = switchPrefsItem(string(R.string.hide_topic_list_popular_summary))\n            .let { root.addView(it.first); it.second }\n        hideTopicListSwitch.isChecked = prefs.getBoolean(\"hide_topic_list_popular\", false)\n\n        val applyToRelateSwitch = switchPrefsItem(string(R.string.apply_to_relate_title))\n            .let { root.addView(it.first); it.second }\n        applyToRelateSwitch.isChecked = prefs.getBoolean(\"home_filter_apply_to_relate\", false)\n\n        root.addView(textInputTitle(string(R.string.hide_low_play_count_recommend_summary)))\n        val lowPlayCountInput = textInputItem(string(R.string.hide_low_play_count_recommend_title))\n            .let { root.addView(it.first); it.second }\n        prefs.getLong(\"hide_low_play_count_recommend_limit\", 0).takeIf { it > 0 }\n            ?.let { lowPlayCountInput.setText(it.toString()) }\n\n        root.addView(textInputTitle(string(R.string.hide_duration_recommend_summary)))\n        val shortDurationInput = textInputItem(string(R.string.hide_short_duration_recommend_title))\n            .let { root.addView(it.first); it.second }\n        val longDurationInput = textInputItem(string(R.string.hide_long_duration_recommend_title))\n            .let { root.addView(it.first); it.second }\n        prefs.getInt(\"hide_short_duration_recommend_limit\", 0).takeIf { it > 0 }\n            ?.let { shortDurationInput.setText(it.toString()) }\n        prefs.getInt(\"hide_long_duration_recommend_limit\", 0).takeIf { it > 0 }\n            ?.let { longDurationInput.setText(it.toString()) }\n\n        root.addView(textInputTitle(string(R.string.keywords_filter_recommend_summary)))\n        val (titleGroup, titleRegexModeSwitch) = root.addKeywordGroup(\n            string(R.string.keyword_group_name_title), showRegex = true\n        )\n        titleRegexModeSwitch.isChecked = prefs.getBoolean(\"home_filter_title_regex_mode\", false)\n        val (reasonGroup, reasonRegexModeSwitch) = root.addKeywordGroup(\n            string(R.string.keyword_group_name_rcmd_reason), showRegex = true\n        )\n        reasonRegexModeSwitch.isChecked = prefs.getBoolean(\"home_filter_reason_regex_mode\", false)\n        val uidGroup = root.addKeywordGroup(\n            string(R.string.keyword_group_name_uid),\n            inputType = EditorInfo.TYPE_CLASS_NUMBER\n        ).first\n        val (upGroup, upRegexModeSwitch) = root.addKeywordGroup(\n            string(R.string.keyword_group_name_up), showRegex = true\n        )\n        upRegexModeSwitch.isChecked = prefs.getBoolean(\"home_filter_up_regex_mode\", false)\n        val categoryGroup = root.addKeywordGroup(string(R.string.keyword_group_name_category)).first\n        val channelGroup = root.addKeywordGroup(string(R.string.keyword_group_name_channel)).first\n        prefs.getStringSet(\"home_filter_keywords_title\", null)?.forEach {\n            titleGroup.addView(keywordInputItem(titleGroup, it).first)\n        }\n        prefs.getStringSet(\"home_filter_keywords_reason\", null)?.forEach {\n            reasonGroup.addView(keywordInputItem(reasonGroup, it).first)\n        }\n        prefs.getStringSet(\"home_filter_keywords_uid\", null)?.forEach {\n            uidGroup.addView(keywordInputItem(uidGroup, it, EditorInfo.TYPE_CLASS_NUMBER).first)\n        }\n        prefs.getStringSet(\"home_filter_keywords_up\", null)?.forEach {\n            upGroup.addView(keywordInputItem(upGroup, it).first)\n        }\n        prefs.getStringSet(\"home_filter_keywords_category\", null)?.forEach {\n            categoryGroup.addView(keywordInputItem(categoryGroup, it).first)\n        }\n        prefs.getStringSet(\"home_filter_keywords_channel\", null)?.forEach {\n            channelGroup.addView(keywordInputItem(channelGroup, it).first)\n        }\n\n        setTitle(string(R.string.home_filter_title))\n\n        setPositiveButton(android.R.string.ok) { _, _ ->\n            val hideTop = hideTopSwitch.isChecked\n            val hideSuggestFollow = hideFollowSwitch.isChecked\n            val hideTopicList = hideTopicListSwitch.isChecked\n\n            val lowPlayCount = lowPlayCountInput.text.toString().toLongOrNull() ?: 0\n            val shortDuration = shortDurationInput.text.toString().toIntOrNull() ?: 0\n            val longDuration = longDurationInput.text.toString().toIntOrNull() ?: 0\n\n            val titles = titleGroup.getKeywords()\n            val titleRegexMode = titleRegexModeSwitch.isChecked\n            if (titleRegexMode && titles.runCatching { forEach { it.toRegex() } }.isFailure) {\n                Log.toast(string(R.string.invalid_regex), force = true)\n                return@setPositiveButton\n            }\n            val reasons = reasonGroup.getKeywords()\n            val reasonRegexMode = reasonRegexModeSwitch.isChecked\n            if (reasonRegexMode && reasons.runCatching { forEach { it.toRegex() } }.isFailure) {\n                Log.toast(string(R.string.invalid_regex), force = true)\n                return@setPositiveButton\n            }\n            val ups = upGroup.getKeywords()\n            val upRegexMode = upRegexModeSwitch.isChecked\n            if (upRegexMode && ups.runCatching { forEach { it.toRegex() } }.isFailure) {\n                Log.toast(string(R.string.invalid_regex), force = true)\n                return@setPositiveButton\n            }\n\n            prefs.edit().apply {\n                putBoolean(\"hide_top_entrance_popular\", hideTop)\n                putBoolean(\"hide_topic_list_popular\", hideTopicList)\n                putBoolean(\"hide_suggest_follow_popular\", hideSuggestFollow)\n                putLong(\"hide_low_play_count_recommend_limit\", lowPlayCount)\n                putInt(\"hide_short_duration_recommend_limit\", shortDuration)\n                putInt(\"hide_long_duration_recommend_limit\", longDuration)\n                putStringSet(\"home_filter_keywords_title\", titles)\n                putStringSet(\"home_filter_keywords_reason\", reasons)\n                putStringSet(\"home_filter_keywords_uid\", uidGroup.getKeywords())\n                putStringSet(\"home_filter_keywords_up\", ups)\n                putStringSet(\"home_filter_keywords_category\", categoryGroup.getKeywords())\n                putStringSet(\"home_filter_keywords_channel\", channelGroup.getKeywords())\n                putBoolean(\"home_filter_title_regex_mode\", titleRegexMode)\n                putBoolean(\"home_filter_reason_regex_mode\", reasonRegexMode)\n                putBoolean(\"home_filter_up_regex_mode\", upRegexMode)\n                putBoolean(\"home_filter_apply_to_relate\", applyToRelateSwitch.isChecked)\n            }.apply()\n\n            Log.toast(string(R.string.prefs_save_success_and_reboot))\n        }\n        setNegativeButton(android.R.string.cancel, null)\n\n        root.setPadding(16.dp, 10.dp, 16.dp, 10.dp)\n\n        setView(scrollView)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/MainActivity.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Bundle\nimport android.preference.Preference\nimport android.preference.Preference.OnPreferenceChangeListener\nimport android.preference.Preference.OnPreferenceClickListener\nimport android.preference.PreferenceCategory\nimport android.preference.PreferenceFragment\nimport android.util.Log\nimport android.view.View\nimport android.widget.Toast\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.launch\nimport me.iacn.biliroaming.hook.SettingHook\nimport me.iacn.biliroaming.utils.fetchJson\n\n\n/**\n * Created by iAcn on 2019/3/23\n * Email i@iacn.me\n */\n\nclass MainActivity : Activity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        fragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()\n    }\n\n    class PrefsFragment : PreferenceFragment(), OnPreferenceChangeListener,\n        OnPreferenceClickListener {\n        private lateinit var runningStatusPref: Preference\n        private val scope = MainScope()\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onCreate(savedInstanceState: Bundle?) {\n            super.onCreate(savedInstanceState)\n            addPreferencesFromResource(R.xml.main_activity)\n            runningStatusPref = findPreference(\"running_status\")\n            findPreference(\"hide_icon\").onPreferenceChangeListener = this\n            findPreference(\"version\").summary = BuildConfig.VERSION_NAME\n            findPreference(\"feature\").onPreferenceClickListener = this\n            findPreference(\"setting\").onPreferenceClickListener = this\n            checkUpdate()\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onDestroy() {\n            super.onDestroy()\n            scope.cancel()\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {\n            when (preference.key) {\n                \"hide_icon\" -> {\n                    val isShow = newValue as Boolean\n                    val aliasName = ComponentName(activity, MainActivity::class.java.name + \"Alias\")\n                    val packageManager = activity.packageManager\n                    val status =\n                        if (isShow) PackageManager.COMPONENT_ENABLED_STATE_DISABLED else PackageManager.COMPONENT_ENABLED_STATE_ENABLED\n                    if (packageManager.getComponentEnabledSetting(aliasName) != status) {\n                        packageManager.setComponentEnabledSetting(\n                            aliasName,\n                            status,\n                            PackageManager.DONT_KILL_APP\n                        )\n                    }\n                }\n            }\n            return true\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onResume() {\n            super.onResume()\n            when {\n                isModuleActive() -> {\n                    runningStatusPref.setTitle(R.string.running_status_enable)\n                    runningStatusPref.setSummary(R.string.runtime_xposed)\n                }\n                isTaiChiModuleActive(activity) -> {\n                    runningStatusPref.setTitle(R.string.running_status_enable)\n                    runningStatusPref.setSummary(R.string.runtime_taichi)\n                }\n                else -> {\n                    runningStatusPref.setTitle(R.string.running_status_disable)\n                    runningStatusPref.setSummary(R.string.not_running_summary)\n                }\n            }\n        }\n\n        private fun checkUpdate() = scope.launch {\n            val result = fetchJson(resources.getString(R.string.version_url)) ?: return@launch\n            val newestVer = result.optString(\"name\")\n            if (newestVer.isNotEmpty() && BuildConfig.VERSION_NAME != newestVer) {\n                findPreference(\"version\").summary = \"${BuildConfig.VERSION_NAME}（最新版$newestVer）\"\n                (findPreference(\"about\") as PreferenceCategory).addPreference(Preference(activity).apply {\n                    key = \"update\"\n                    title = resources.getString(R.string.update_title)\n                    summary = result.optString(\"body\").substringAfterLast(\"更新日志\\r\\n\")\n                        .ifEmpty { resources.getString(R.string.update_summary) }\n                    onPreferenceClickListener = this@PrefsFragment\n                    order = 1\n                })\n            }\n        }\n\n        private fun onUpdateCheck(): Boolean {\n            val uri = Uri.parse(resources.getString(R.string.update_url))\n            val intent = Intent(Intent.ACTION_VIEW, uri)\n            startActivity(intent)\n            return true\n        }\n\n        private fun onFeatureClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                setView(View.inflate(activity, R.layout.feature, null))\n                setNegativeButton(\"关闭\", null)\n                show()\n            }\n            return true\n        }\n\n        private fun onSettingClick(): Boolean {\n            val packages = Constant.BILIBILI_PACKAGE_NAME.filter { isPackageInstalled(it.value) }\n            when {\n                packages.size == 1 -> startSetting(packages.values.first())\n                packages.isEmpty() -> Toast.makeText(activity, \"未检测到已安装的客户端\", Toast.LENGTH_LONG)\n                    .show()\n                else -> {\n                    AlertDialog.Builder(activity).run {\n                        val keys = packages.keys.toTypedArray()\n                        setItems(keys) { _, i -> startSetting(packages[keys[i]] ?: error(\"\")) }\n                        setTitle(\"请选择版本\")\n                        show()\n                    }\n                }\n            }\n            return true\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onPreferenceClick(preference: Preference?) = when (preference?.key) {\n            \"update\" -> onUpdateCheck()\n            \"feature\" -> onFeatureClick()\n            \"setting\" -> onSettingClick()\n            else -> false\n        }\n\n        private fun isPackageInstalled(packageName: String) = try {\n            activity.packageManager.getPackageInfo(packageName, 0)\n            true\n        } catch (e: PackageManager.NameNotFoundException) {\n            false\n        }\n\n        private fun startSetting(packageName: String) {\n            activity.packageManager.getLaunchIntentForPackage(packageName)?.run {\n                addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)\n                putExtra(SettingHook.START_SETTING_KEY, true)\n                startActivity(this)\n            }\n        }\n    }\n\n    companion object {\n        fun isModuleActive(): Boolean {\n            Log.i(\"大不自多\", \"海纳江河\")\n            return false\n        }\n\n        private fun isTaiChiModuleActive(context: Context): Boolean {\n            val contentResolver = context.contentResolver\n            val uri = Uri.parse(\"content://me.weishu.exposed.CP/\")\n            return try {\n                val result = contentResolver.call(uri, \"active\", null, null)\n                result?.getBoolean(\"active\", false) ?: false\n            } catch (e: Exception) {\n                false\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/SearchFilterDialog.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.content.SharedPreferences\nimport android.view.inputmethod.EditorInfo\nimport android.widget.*\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.dp\n\nclass SearchFilterDialog(activity: Activity, prefs: SharedPreferences) :\n    BaseWidgetDialog(activity) {\n    init {\n        val scrollView = ScrollView(context).apply {\n            scrollBarStyle = ScrollView.SCROLLBARS_OUTSIDE_OVERLAY\n        }\n        val root = LinearLayout(context).apply {\n            orientation = LinearLayout.VERTICAL\n            layoutParams = FrameLayout.LayoutParams(\n                FrameLayout.LayoutParams.MATCH_PARENT,\n                FrameLayout.LayoutParams.WRAP_CONTENT\n            )\n        }\n        scrollView.addView(root)\n\n        val categoryVideoTitle = categoryTitle(string(R.string.filter_search_video))\n        root.addView(categoryVideoTitle)\n\n        val (contentGroup, contentRegexSwitch) = root.addKeywordGroup(\n            string(R.string.keyword_group_name_content), 40.dp, true\n        )\n        contentRegexSwitch.isChecked = prefs.getBoolean(\"search_filter_content_regex_mode\", false)\n        val upNameGroup = root.addKeywordGroup(string(R.string.keyword_group_name_up), 40.dp).first\n        val uidGroup = root.addKeywordGroup(\n            string(R.string.keyword_group_name_uid),\n            40.dp,\n            inputType = EditorInfo.TYPE_CLASS_NUMBER\n        ).first\n        prefs.getStringSet(\"search_filter_keyword_content\", null)?.forEach {\n            contentGroup.addView(keywordInputItem(contentGroup, it).first)\n        }\n        prefs.getStringSet(\"search_filter_keyword_upname\", null)?.forEach {\n            upNameGroup.addView(keywordInputItem(upNameGroup, it).first)\n        }\n        prefs.getStringSet(\"search_filter_keyword_uid\", null)?.forEach {\n            uidGroup.addView(keywordInputItem(uidGroup, it, EditorInfo.TYPE_CLASS_NUMBER).first)\n        }\n\n        val removeRelatePromoteSwitch = switchPrefsItem(string(R.string.filter_search_remove_relate_promote))\n            .let { root.addView(it.first); it.second }\n        removeRelatePromoteSwitch.isChecked = prefs.getBoolean(\"search_filter_remove_relate_promote\", false)\n\n        setTitle(string(R.string.filter_search_title))\n\n        setPositiveButton(android.R.string.ok) { _, _ ->\n\n            val contents = contentGroup.getKeywords()\n            val contentRegexMode = contentRegexSwitch.isChecked\n            if (contentRegexMode && contents.runCatching { forEach { it.toRegex() } }.isFailure) {\n                Log.toast(string(R.string.invalid_regex), force = true)\n                return@setPositiveButton\n            }\n\n            prefs.edit().apply {\n                putStringSet(\"search_filter_keyword_content\", contents)\n                putStringSet(\"search_filter_keyword_upname\", upNameGroup.getKeywords())\n                putStringSet(\"search_filter_keyword_uid\", uidGroup.getKeywords())\n                putBoolean(\"search_filter_content_regex_mode\", contentRegexMode)\n                putBoolean(\"search_filter_remove_relate_promote\", removeRelatePromoteSwitch.isChecked)\n            }.apply()\n            Log.toast(string(R.string.prefs_save_success_and_reboot))\n        }\n        setNegativeButton(android.R.string.cancel, null)\n\n        root.setPadding(16.dp, 10.dp, 16.dp, 10.dp)\n\n        setView(scrollView)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/SettingDialog.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.Activity.RESULT_CANCELED\nimport android.app.AlertDialog\nimport android.content.ActivityNotFoundException\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.SharedPreferences\nimport android.content.res.Resources\nimport android.graphics.Bitmap\nimport android.graphics.Color\nimport android.graphics.Typeface\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.preference.*\nimport android.provider.MediaStore\nimport android.text.Editable\nimport android.text.SpannableStringBuilder\nimport android.text.Spanned\nimport android.text.TextWatcher\nimport android.text.style.AbsoluteSizeSpan\nimport android.text.style.ForegroundColorSpan\nimport android.text.style.StyleSpan\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport android.view.inputmethod.EditorInfo\nimport android.view.inputmethod.InputMethodManager\nimport android.widget.*\nimport android.widget.SeekBar.OnSeekBarChangeListener\nimport android.widget.Toast\nimport androidx.documentfile.provider.DocumentFile\nimport kotlinx.coroutines.*\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.hook.JsonHook\nimport me.iacn.biliroaming.hook.SplashHook\nimport me.iacn.biliroaming.utils.*\nimport me.iacn.biliroaming.utils.UposReplaceHelper.isLocatedCn\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.net.URL\nimport kotlin.system.exitProcess\n\n\nclass SettingDialog(context: Context) : AlertDialog.Builder(context) {\n\n    class PrefsFragment : PreferenceFragment(), Preference.OnPreferenceChangeListener,\n        Preference.OnPreferenceClickListener {\n        private val scope = MainScope()\n        private lateinit var prefs: SharedPreferences\n        private lateinit var biliprefs: SharedPreferences\n        private var counter: Int = 0\n        private var customSubtitleDialog: CustomSubtitleDialog? = null\n        private lateinit var listView: ListView\n        private lateinit var adapter: BaseAdapter\n        private var searchItems = listOf<SearchItem>()\n\n        private var ListAdapter.preferenceList: List<Preference>\n            get() = getObjectFieldAs(\"mPreferenceList\")\n            set(value) {\n                setObjectField(\"mPreferenceList\", value)\n            }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onCreate(savedInstanceState: Bundle?) {\n            super.onCreate(savedInstanceState)\n            preferenceManager.sharedPreferencesName = \"biliroaming\"\n            prefs = preferenceManager.sharedPreferences\n            checkUposServer()\n            addPreferencesFromResource(R.xml.prefs_setting)\n            biliprefs = currentContext.getSharedPreferences(\n                packageName + \"_preferences\",\n                Context.MODE_MULTI_PROCESS\n            )\n            if (!prefs.getBoolean(\"hidden\", false)) {\n                val hiddenGroup = findPreference(\"hidden_group\") as PreferenceCategory\n                preferenceScreen.removePreference(hiddenGroup)\n            }\n            findPreference(\"version\")?.summary = BuildConfig.VERSION_NAME\n            findPreference(\"version\")?.onPreferenceClickListener = this\n            findPreference(\"custom_splash\")?.onPreferenceChangeListener = this\n            findPreference(\"custom_splash_logo\")?.onPreferenceChangeListener = this\n            findPreference(\"save_log\")?.summary =\n                context.getString(R.string.save_log_summary).format(logFile.absolutePath)\n            findPreference(\"custom_server\")?.onPreferenceClickListener = this\n            findPreference(\"test_upos\")?.onPreferenceClickListener = this\n            findPreference(\"customize_bottom_bar\")?.onPreferenceClickListener = this\n            findPreference(\"pref_export\")?.onPreferenceClickListener = this\n            findPreference(\"pref_import\")?.onPreferenceClickListener = this\n            findPreference(\"export_video\")?.onPreferenceClickListener = this\n            findPreference(\"home_filter\")?.onPreferenceClickListener = this\n            findPreference(\"custom_subtitle\")?.onPreferenceChangeListener = this\n            findPreference(\"danmaku_filter\")?.onPreferenceClickListener = this\n            findPreference(\"default_speed\").onPreferenceClickListener = this\n            findPreference(\"customize_accessKey\")?.onPreferenceClickListener = this\n            findPreference(\"share_log\")?.onPreferenceClickListener = this\n            findPreference(\"customize_drawer\")?.onPreferenceClickListener = this\n            findPreference(\"custom_link\")?.onPreferenceClickListener = this\n            findPreference(\"add_custom_button\")?.onPreferenceChangeListener = this\n            findPreference(\"customize_dynamic\")?.onPreferenceClickListener = this\n            findPreference(\"filter_search\")?.onPreferenceClickListener = this\n            findPreference(\"filter_comment\")?.onPreferenceClickListener = this\n            findPreference(\"copy_access_key\")?.onPreferenceClickListener = this\n            findPreference(\"purify_story_video_ad\")?.onPreferenceClickListener = this\n            findPreference(\"long_press_speed\")?.onPreferenceClickListener = this\n            checkCompatibleVersion()\n            searchItems = retrieve(preferenceScreen)\n            checkUpdate()\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onDestroy() {\n            super.onDestroy()\n            scope.cancel()\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onActivityCreated(savedInstanceState: Bundle?) {\n            super.onActivityCreated(savedInstanceState)\n            listView = view?.findViewById(android.R.id.list) ?: return\n            adapter = listView.adapter as BaseAdapter\n        }\n\n        private fun retrieve(group: PreferenceGroup): List<SearchItem> = buildList {\n            for (i in 0 until group.preferenceCount) {\n                val preference = group.getPreference(i)\n                val entries = when (preference) {\n                    is ListPreference -> preference.entries\n                    is MultiSelectListPreference -> preference.entries\n                    else -> arrayOf()\n                }.orEmpty()\n                val searchItem = SearchItem(\n                    preference,\n                    preference.key.orEmpty(),\n                    preference.title ?: \"\",\n                    preference.summary ?: \"\",\n                    entries,\n                    preference is PreferenceGroup,\n                )\n                searchItem.appendExtraKeywords()\n                add(searchItem)\n                if (preference is PreferenceGroup) {\n                    addAll(retrieve(preference))\n                }\n            }\n        }\n\n        private fun SearchItem.appendExtraKeywords() = when (key) {\n            \"custom_subtitle\" -> {\n                extra.add(context.getString(R.string.custom_subtitle_remove_bg))\n                extra.add(context.getString(R.string.custom_subtitle_bold))\n                extra.add(context.getString(R.string.custom_subtitle_font_size))\n                extra.add(context.getString(R.string.custom_subtitle_stroke_color))\n                extra.add(context.getString(R.string.custom_subtitle_stroke_width))\n                extra.add(context.getString(R.string.custom_subtitle_offset))\n            }\n\n            \"home_filter\" -> {\n                extra.add(context.getString(R.string.apply_to_relate_title))\n                extra.add(context.getString(R.string.hide_low_play_count_recommend_title))\n                extra.add(context.getString(R.string.hide_low_play_count_recommend_summary))\n                extra.add(context.getString(R.string.hide_short_duration_recommend_title))\n                extra.add(context.getString(R.string.hide_long_duration_recommend_title))\n                extra.add(context.getString(R.string.hide_duration_recommend_summary))\n                extra.add(context.getString(R.string.keywords_filter_recommend_summary))\n            }\n\n            \"customize_bottom_bar\" -> {\n                extra.addAll(JsonHook.bottomItems.mapNotNull { it.name })\n            }\n\n            \"customize_drawer\" -> {\n                extra.addAll(JsonHook.drawerItems.mapNotNull { it.name })\n            }\n\n            \"customize_dynamic\" -> {\n                extra.add(context.getString(R.string.customize_dynamic_prefer_video_tab))\n                extra.add(context.getString(R.string.purify_city_title))\n                extra.add(context.getString(R.string.purify_campus_title))\n                extra.add(context.getString(R.string.customize_dynamic_all_rm_topic_title))\n                extra.add(context.getString(R.string.customize_dynamic_all_rm_up_title))\n                extra.add(context.getString(R.string.customize_dynamic_video_rm_up_title))\n                extra.add(context.getString(R.string.customize_dynamic_filter_apply_to_video))\n                extra.add(context.getString(R.string.customize_dynamic_rm_blocked_title))\n                extra.addAll(context.resources.getStringArray(R.array.dynamic_entries))\n            }\n\n            \"pref_import\", \"pref_export\" -> {\n                extra.add(context.getString(R.string.pref_backup))\n            }\n\n            else -> false\n        }\n\n        fun search(text: String) {\n            val preferences = if (text.isEmpty()) {\n                searchItems.map { it.restore(); it.preference }\n            } else {\n                searchItems.sortedByDescending { it.calcScoreAndApplyHintBy(text) }\n                    .filterNot { it.cacheScore == 0 }.map { it.preference }\n            }\n            adapter.preferenceList = preferences\n            adapter.notifyDataSetChanged()\n            listView.forceSetSelection(0)\n        }\n\n        private fun checkUposServer() {\n            val currentServer = prefs.getString(\"upos_host\", null).orEmpty()\n            val serverList = context.resources.getStringArray(R.array.upos_values)\n            if (currentServer !in serverList) {\n                scope.launch(Dispatchers.IO) {\n                    val defaultServer =\n                        if (isLocatedCn) serverList[1] else \"\"\"$1\"\"\"\n                    prefs.edit().putString(\"upos_host\", defaultServer).apply()\n                }\n            }\n        }\n\n        private fun checkUpdate() {\n            val url = URL(context.getString(R.string.version_url))\n            scope.launch {\n                val result = fetchJson(url) ?: return@launch\n                val newestVer = result.optString(\"name\")\n                val versionName = BuildConfig.VERSION_NAME\n                if (newestVer.isNotEmpty() && versionName != newestVer) {\n                    searchItems.forEach { it.restore() }\n                    findPreference(\"version\").summary = \"${versionName}（最新版$newestVer）\"\n                    (findPreference(\"about\") as PreferenceCategory).addPreference(\n                        Preference(activity).apply {\n                            key = \"update\"\n                            title = context.getString(R.string.update_title)\n                            summary = result.optString(\"body\").substringAfterLast(\"更新日志\\r\\n\")\n                                .ifEmpty { context.getString(R.string.update_summary) }\n                            onPreferenceClickListener = this@PrefsFragment\n                            order = 1\n                        })\n                    searchItems = retrieve(preferenceScreen)\n                }\n            }\n        }\n\n        private fun checkCompatibleVersion() {\n            val versionCode = getVersionCode(packageName)\n            var supportMusicNotificationHook = versionCode >= 7500300 &&\n                    // from bilibili\n                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Build.MANUFACTURER.lowercase().equals(\"huawei\")\n            var supportCustomizeTab = true\n            val supportFullSplash = try {\n                instance.splashInfoClass?.getMethod(\"getMode\") != null\n            } catch (e: Throwable) {\n                false\n            }\n            val supportMain = !isBuiltIn || !is64 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O\n            var supportDrawer = instance.homeUserCenterClass != null\n            var supportDrawerStyle = true\n            val supportRevertLive = versionCode < 6830000\n            var supportAddTag = true\n            var supportCustomTheme = true\n            when (platform) {\n                \"android_hd\" -> {\n                    supportCustomizeTab = false\n                    supportDrawer = false\n                    supportDrawerStyle = false\n                    supportAddTag = false\n                    supportCustomTheme = false\n                }\n            }\n            val supportSplashHook = instance.brandSplashClass != null\n            val supportTeenagersMode = instance.teenagersModeDialogActivityClass != null\n            val supportStoryVideo = instance.storyVideoActivityClass != null\n            val supportPurifyShare = instance.shareClickResultClass != null\n            val supportDownloadThread = versionCode < 6630000 || versionCode >= 6900000\n            if (!supportDrawer)\n                disablePreference(\"drawer\")\n            if (!supportSplashHook) {\n                disablePreference(\"custom_splash\")\n                disablePreference(\"custom_splash_logo\")\n            }\n            if (!supportFullSplash) {\n                disablePreference(\"full_splash\")\n            }\n            if (!supportMusicNotificationHook) {\n                if (versionCode >= 7500300) {\n                    disablePreference(\n                            \"music_notification\",\n                            context.getString(R.string.os_not_support))\n                } else {\n                    disablePreference(\"music_notification\")\n                }\n            }\n            if (!supportMain) {\n                disablePreference(\"main_func\", \"Android O以下系统不支持64位Xpatch版，请使用32位版\")\n            }\n            if (!supportTeenagersMode) {\n                disablePreference(\"teenagers_mode_dialog\")\n            }\n            if (!supportCustomizeTab) {\n                disablePreference(\"customize_home_tab_title\")\n                disablePreference(\"customize_bottom_bar_title\")\n            }\n            if (!supportStoryVideo) {\n                disablePreference(\"replace_story_video\")\n            }\n            if (!supportDrawerStyle) {\n                disablePreference(\"drawer_style_switch\")\n                disablePreference(\"drawer_style\")\n            }\n            if (!supportPurifyShare) {\n                disablePreference(\"purify_share\")\n                disablePreference(\"mini_program\")\n            }\n            if (!supportDownloadThread) {\n                disablePreference(\"custom_download_thread\")\n            }\n            if (!supportRevertLive) {\n                disablePreference(\"revert_live_room_feed\")\n            }\n            if (!supportAddTag) {\n                disablePreference(\"add_bangumi\")\n                disablePreference(\"add_korea\")\n                disablePreference(\"add_movie\")\n            }\n            if (!supportCustomTheme) {\n                disablePreference(\"custom_theme\")\n            }\n        }\n\n        private fun disablePreference(\n            name: String,\n            message: String = context.getString(R.string.not_support)\n        ) {\n            findPreference(name)?.run {\n                isEnabled = false\n                summary = message\n                if (this is SwitchPreference) this.isChecked = false\n            }\n        }\n\n        private fun showCustomSubtitle() {\n            CustomSubtitleDialog(activity, this, prefs).also {\n                customSubtitleDialog = it\n            }.show()\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {\n            when (preference.key) {\n                \"custom_splash\" -> {\n                    if (newValue as Boolean)\n                        selectImage(SPLASH_SELECTION)\n                }\n\n                \"custom_splash_logo\" -> {\n                    if (newValue as Boolean)\n                        selectImage(LOGO_SELECTION)\n                }\n\n                \"custom_subtitle\" -> {\n                    if (newValue as Boolean)\n                        showCustomSubtitle()\n                }\n\n                \"add_custom_button\" -> {\n                    if (newValue as Boolean)\n                        onAddCustomButtonClick()\n                }\n            }\n            return true\n        }\n\n        private fun selectImage(action: Int): Boolean {\n            val intent = Intent(Intent.ACTION_GET_CONTENT)\n            intent.type = \"*/*\"\n            intent.addCategory(Intent.CATEGORY_OPENABLE)\n\n            try {\n                startActivityForResult(Intent.createChooser(intent, \"选择一张图片\"), action)\n            } catch (ex: ActivityNotFoundException) {\n                Log.toast(\"请安装文件管理器\")\n            }\n            return true\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n            customSubtitleDialog?.onActivityResult(requestCode, resultCode, data)\n            when (requestCode) {\n                SPLASH_SELECTION, LOGO_SELECTION -> {\n                    val destFile = when (requestCode) {\n                        SPLASH_SELECTION ->\n                            File(currentContext.filesDir, SplashHook.SPLASH_IMAGE)\n\n                        LOGO_SELECTION ->\n                            File(currentContext.filesDir, SplashHook.LOGO_IMAGE)\n\n                        else -> null\n                    } ?: return\n                    val uri = data?.data\n                    if (resultCode == RESULT_CANCELED || uri == null) {\n                        destFile.delete()\n                        return\n                    }\n                    val stream = ByteArrayOutputStream()\n                    stream.flush()\n                    MediaStore.Images.Media.getBitmap(activity.contentResolver, uri)\n                        .compress(Bitmap.CompressFormat.PNG, 100, stream)\n                    val dest = FileOutputStream(destFile)\n                    stream.writeTo(dest)\n                }\n\n                PREF_EXPORT, PREF_IMPORT -> {\n                    val file = File(currentContext.filesDir, \"../shared_prefs/biliroaming.xml\")\n                    val uri = data?.data\n                    if (resultCode == RESULT_CANCELED || uri == null) return\n                    when (requestCode) {\n                        PREF_IMPORT -> {\n                            try {\n                                file.bufferedWriter().use { output ->\n                                    activity.contentResolver.openInputStream(uri)\n                                        ?.use { it.bufferedReader().copyTo(output) }\n                                }\n                            } catch (e: Exception) {\n                                Log.toast(e.message ?: \"未知错误\", true, alsoLog = true)\n                            }\n                            Log.toast(\"请至少重新打开哔哩漫游设置\", true)\n                        }\n\n                        PREF_EXPORT -> {\n                            try {\n                                file.bufferedReader().use { input ->\n                                    activity.contentResolver.openOutputStream(uri)?.use {\n                                        it.bufferedWriter().use { output ->\n                                            input.copyTo(output)\n                                        }\n                                    }\n                                }\n                            } catch (e: Exception) {\n                                Log.toast(e.message ?: \"未知错误\", true, alsoLog = true)\n                            }\n                        }\n                    }\n                }\n\n                VIDEO_EXPORT -> {\n                    val videosToExport = VideoExportDialog.videosToExport\n                    VideoExportDialog.videosToExport = emptySet()\n                    val uri = data?.data\n                    if (resultCode == RESULT_CANCELED || uri == null) return\n                    val targetDir = DocumentFile.fromTreeUri(activity, uri) ?: return\n                    try {\n                        videosToExport.forEach { video ->\n                            targetDir.findOrCreateDir(video.parentFile!!.name)\n                                ?.let { DocumentFile.fromFile(video).copyTo(it) }\n                        }\n                        Log.toast(\"导出成功\", true)\n                    } catch (e: Exception) {\n                        Log.toast(\"${e.message}\", alsoLog = true)\n                    }\n                }\n            }\n\n            super.onActivityResult(requestCode, resultCode, data)\n        }\n\n\n        private fun onVersionClick(): Boolean {\n            if (prefs.getBoolean(\"hidden\", false) || counter == 7) return true\n            if (++counter == 7) {\n                prefs.edit()?.putBoolean(\"hidden\", true)?.apply()\n                Log.toast(\"已开启隐藏功能，重启应用生效\", true)\n            } else if (counter >= 4) {\n                Log.toast(\"再按${7 - counter}次开启隐藏功能\", true)\n            }\n\n            return true\n        }\n\n        private fun onUpdateClick(): Boolean {\n            val uri = Uri.parse(context.getString(R.string.update_url))\n            val intent = Intent(Intent.ACTION_VIEW, uri)\n            startActivity(intent)\n            return true\n        }\n\n        private fun onCustomServerClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                val view = context.inflateLayout(R.layout.customize_backup_dialog)\n                val editTexts = arrayOf(\n                    view.findViewById<EditText>(R.id.cn_server),\n                    view.findViewById(R.id.hk_server),\n                    view.findViewById(R.id.tw_server),\n                    view.findViewById(R.id.th_server)\n                )\n                editTexts.forEach { it.setText(prefs.getString(it.tag.toString(), \"\")) }\n                setTitle(\"设置解析服务器\")\n                setView(view)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    editTexts.forEach {\n                        val host = it.text.toString()\n                        if (host.isNotEmpty())\n                            prefs.edit().putString(\n                                it.tag.toString(),\n                                host.replace(Regex(\"^https?://\"), \"\")\n                            ).apply()\n                        else\n                            prefs.edit().remove(it.tag.toString()).apply()\n                    }\n                }\n                setNegativeButton(\"获取公共解析服务器\") { _, _ ->\n                    val uri = Uri.parse(context.getString(R.string.server_url))\n                    val intent = Intent(Intent.ACTION_VIEW, uri)\n                    startActivity(intent)\n                }\n                show()\n            }\n            return true\n        }\n\n        private fun onDanmakuFilterClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                val view = context.inflateLayout(R.layout.seekbar_dialog)\n                val seekBar = view.findViewById<SeekBar>(R.id.seekBar)\n                seekBar.max = 12\n                val tvHint = view.findViewById<TextView>(R.id.tvHint)\n                seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {\n                    override fun onProgressChanged(\n                        seekBar: SeekBar?, progress: Int, fromUser: Boolean\n                    ) {\n                        tvHint.text =\n                            context.getString(R.string.danmaku_filter_weight_hint, progress)\n                    }\n\n                    override fun onStartTrackingTouch(seekBar: SeekBar?) {}\n                    override fun onStopTrackingTouch(seekBar: SeekBar?) {}\n                })\n                val current = prefs.getInt(\"danmaku_filter_weight\", 0)\n                tvHint.text = context.getString(R.string.danmaku_filter_weight_hint, current)\n                seekBar.progress = current\n                setTitle(R.string.danmaku_filter_title)\n                setNegativeButton(android.R.string.cancel, null)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    prefs.edit().putInt(\"danmaku_filter_weight\", seekBar.progress).apply()\n                }\n                setView(view)\n                show()\n            }\n            return true\n        }\n\n        private fun onDefaultSpeedClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                val view = context.inflateLayout(R.layout.seekbar_dialog)\n                val seekBar = view.findViewById<SeekBar>(R.id.seekBar)\n                seekBar.max = 100\n                val tvHint = view.findViewById<TextView>(R.id.tvHint)\n                seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {\n                    @SuppressLint(\"SetTextI18n\")\n                    override fun onProgressChanged(\n                        seekBar: SeekBar?, progress: Int, fromUser: Boolean\n                    ) {\n                        tvHint.text = \"${progress * 10}%\"\n                    }\n\n                    override fun onStartTrackingTouch(seekBar: SeekBar?) {}\n                    override fun onStopTrackingTouch(seekBar: SeekBar?) {}\n                })\n                val current = prefs.getInt(\"default_speed\", 100)\n                @SuppressLint(\"SetTextI18n\")\n                tvHint.text = \"${current * 10}%\"\n                seekBar.progress = current / 10\n                setTitle(R.string.default_speed_title)\n                setNegativeButton(android.R.string.cancel, null)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    prefs.edit().putInt(\"default_speed\", seekBar.progress * 10).apply()\n                }\n                setView(view)\n                show()\n\n            }\n            return true\n        }\n\n        private fun onTestUposClick(): Boolean {\n            SpeedTestDialog(activity, prefs).show()\n            return true\n        }\n\n        private fun onCustomizeBottomBarClick(): Boolean {\n            AlertDialog.Builder(activity).apply {\n                val bottomItems = JsonHook.bottomItems\n                val ids = bottomItems.map { it.id }.toHashSet()\n                sPrefs.getStringSet(\"hided_bottom_items\", null)?.forEach {\n                    if (it.isEmpty() || ids.contains(it)) return@forEach\n                    bottomItems.add(JsonHook.BottomItem(\"未知\", null, it, false))\n                }\n                setTitle(context.getString(R.string.customize_bottom_bar_title))\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    val hideItems = mutableSetOf<String>()\n                    bottomItems.forEach {\n                        if (it.showing.not()) {\n                            hideItems.add(it.id ?: \"\")\n                        }\n                    }\n                    sPrefs.edit().putStringSet(\"hided_bottom_items\", hideItems).apply()\n                }\n                setNegativeButton(android.R.string.cancel, null)\n                val names = Array(bottomItems.size) { i ->\n                    \"${bottomItems[i].name} (${bottomItems[i].id}) (${bottomItems[i].uri})\"\n                }\n                setNeutralButton(\"重置\") { _, _ ->\n                    sPrefs.edit().remove(\"hided_bottom_items\").apply()\n                }\n                val showings = BooleanArray(bottomItems.size) { i ->\n                    bottomItems[i].showing\n                }\n                setMultiChoiceItems(names, showings) { _, which, isChecked ->\n                    bottomItems[which].showing = isChecked\n                }\n            }.show()\n            return true\n        }\n\n        private fun onPrefExportClick(): Boolean {\n            val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)\n            intent.type = \"text/xml\"\n            intent.putExtra(Intent.EXTRA_TITLE, \"biliroaming.xml\")\n            intent.addCategory(Intent.CATEGORY_OPENABLE)\n            try {\n                startActivityForResult(Intent.createChooser(intent, \"保存配置文件\"), PREF_EXPORT)\n            } catch (ex: ActivityNotFoundException) {\n                Log.toast(\"请安装文件管理器\")\n            }\n            return true\n        }\n\n        private fun onPrefImportClick(): Boolean {\n            val intent = Intent(Intent.ACTION_GET_CONTENT)\n            intent.type = \"text/xml\"\n            intent.addCategory(Intent.CATEGORY_OPENABLE)\n            try {\n                startActivityForResult(Intent.createChooser(intent, \"选择配置文件\"), PREF_IMPORT)\n            } catch (ex: ActivityNotFoundException) {\n                Log.toast(\"请安装文件管理器\")\n            }\n            return true\n        }\n\n        private fun onExportVideoClick(): Boolean {\n            VideoExportDialog(activity, this).show()\n            return true\n        }\n\n        private fun onCustomizeAccessKeyClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                val view = context.inflateLayout(R.layout.customize_backup_dialog)\n                val editTexts = arrayOf(\n                    view.findViewById<EditText>(R.id.cn_server),\n                    view.findViewById(R.id.hk_server),\n                    view.findViewById(R.id.tw_server),\n                    view.findViewById(R.id.th_server)\n                )\n                editTexts.forEach {\n                    val accessKey = prefs.getString(\"${it.tag}_accessKey\", \"\")\n                    val platform = prefs.getString(\"${it.tag}_platform\", null)\n                    val s = if (platform != null) \"$accessKey;$platform\" else accessKey\n                    it.setText(s)\n                    it.hint = \"\"\n                }\n                setTitle(R.string.customize_accessKey_title)\n                setView(view)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    val edit = prefs.edit()\n                    editTexts.forEach {\n                        val s = it.text.toString()\n                        val accessKey = s.substringBefore(';')\n                        val platform = s.substringAfter(';', \"\")\n                        val platformKey = \"${it.tag}_platform\"\n                        val key = \"${it.tag}_accessKey\"\n                        if (accessKey.isNotEmpty()) edit.putString(key, accessKey).apply()\n                        else edit.remove(key).apply()\n                        if (platform.isNotEmpty()) edit.putString(platformKey, platform).apply()\n                        else edit.remove(platformKey).apply()\n                    }\n                }\n                show()\n            }\n            return true\n        }\n\n        private fun onHomeFilterClick(): Boolean {\n            HomeFilterDialog(activity, prefs).show()\n            return true\n        }\n\n        private fun onShareLogClick(): Boolean {\n            if ((logFile.exists().not() && oldLogFile.exists().not()) || shouldSaveLog.not()) {\n                Log.toast(\"没有保存过日志\", force = true)\n                return true\n            }\n            AlertDialog.Builder(activity)\n                .setTitle(context.getString(R.string.share_log_title))\n                .setItems(arrayOf(\"log.txt\", \"old_log.txt (崩溃相关发这个)\")) { _, which ->\n                    val toShareLog = if (which == 0) logFile else oldLogFile\n                    if (toShareLog.exists()) {\n                        toShareLog.copyTo(\n                            File(activity.cacheDir, \"boxing/log.txt\"),\n                            overwrite = true\n                        )\n                        val uri =\n                            Uri.parse(\"content://${activity.packageName}.fileprovider/internal/log.txt\")\n                        activity.startActivity(Intent.createChooser(Intent().apply {\n                            action = Intent.ACTION_SEND\n                            putExtra(Intent.EXTRA_STREAM, uri)\n                            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                            setDataAndType(uri, \"text/log\")\n                        }, context.getString(R.string.share_log_title)))\n                    } else {\n                        Log.toast(\"日志文件不存在\", force = true)\n                    }\n                }\n                .show()\n            return true\n        }\n\n        private fun onCustomizeDrawerClick(): Boolean {\n            AlertDialog.Builder(activity).apply {\n                val drawerItems = JsonHook.drawerItems\n                val ids = drawerItems.map { it.id }.toHashSet()\n                sPrefs.getStringSet(\"hided_drawer_items\", null)?.forEach {\n                    if (it.isEmpty() || ids.contains(it)) return@forEach\n                    JsonHook.drawerItems.add(JsonHook.BottomItem(\"未知\", null, it, false))\n                }\n                setTitle(context.getString(R.string.customize_drawer_title))\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    val hideItems = mutableSetOf<String>()\n                    JsonHook.drawerItems.forEach {\n                        if (it.showing.not()) {\n                            hideItems.add(it.id ?: \"\")\n                        }\n                    }\n                    sPrefs.edit().putStringSet(\"hided_drawer_items\", hideItems).apply()\n                }\n                setNegativeButton(android.R.string.cancel, null)\n                val names = Array(drawerItems.size) { i ->\n                    \"${drawerItems[i].name} (${drawerItems[i].id}) (${drawerItems[i].uri})\"\n                }\n                setNeutralButton(\"重置\") { _, _ ->\n                    sPrefs.edit().remove(\"hided_drawer_items\").apply()\n                }\n                val showings = BooleanArray(drawerItems.size) { i ->\n                    drawerItems[i].showing\n                }\n                setMultiChoiceItems(names, showings) { _, which, isChecked ->\n                    drawerItems[which].showing = isChecked\n                }\n            }.show()\n            return true\n        }\n\n        private fun onCustomLinkClick(): Boolean {\n            val tv = EditText(activity)\n            tv.setText(sPrefs.getString(\"custom_link\", \"\"))\n            tv.hint = \"bilibili://user_center/vip\"\n            AlertDialog.Builder(activity).run {\n                setTitle(context.getString(R.string.custom_link_summary))\n                setView(tv)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    if (tv.text.toString().startsWith(\"bilibili://\")) {\n                        sPrefs.edit().putString(\"custom_link\", tv.text.toString()).apply()\n                        val intent = Intent(\n                            Intent.ACTION_VIEW,\n                            Uri.parse(sPrefs.getString(\"custom_link\", \"\"))\n                        )\n                        startActivity(intent)\n                    } else {\n                        Log.toast(\"格式不正确\", force = true)\n                    }\n                }\n                setNeutralButton(\"清空\") { _, _ ->\n                    sPrefs.edit().remove(\"custom_link\").apply()\n                }\n                setNegativeButton(android.R.string.cancel, null)\n                show()\n            }\n            return true\n        }\n\n        private fun onAddCustomButtonClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                val view = context.inflateLayout(R.layout.custom_button)\n                val editTexts = arrayOf(\n                    view.findViewById<EditText>(R.id.custom_button_id),\n                    view.findViewById(R.id.custom_button_title),\n                    view.findViewById(R.id.custom_button_uri),\n                    view.findViewById(R.id.custom_button_icon)\n                )\n                editTexts.forEach {\n                    it.setText(prefs.getString(\"${it.tag}\", \"\"))\n                }\n                setTitle(R.string.add_custom_button_title)\n                setView(view)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    editTexts.forEach {\n                        val key = \"${it.tag}\"\n                        val value = it.text.toString()\n                        if (value.isNotEmpty()) prefs.edit().putString(key, value).apply()\n                        else prefs.edit().remove(key).apply()\n                    }\n                }\n                show()\n            }\n            return true\n        }\n\n        private fun onCustomDynamicClick(): Boolean {\n            DynamicFilterDialog(activity, prefs).create().also { dialog ->\n                dialog.setOnShowListener {\n                    dialog.window?.clearFlags(\n                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n                                or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM\n                    )\n                    dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)\n                }\n            }.show()\n            return true\n        }\n\n        private fun onFilterSearchClick(): Boolean {\n            SearchFilterDialog(activity, prefs).create().also { dialog ->\n                dialog.setOnShowListener {\n                    dialog.window?.clearFlags(\n                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n                                or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM\n                    )\n                    dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)\n                }\n            }.show()\n            return true\n        }\n\n        private fun onFilterCommentClick(): Boolean {\n            CommentFilterDialog(activity, prefs).create().also { dialog ->\n                dialog.setOnShowListener {\n                    dialog.window?.clearFlags(\n                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n                                or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM\n                    )\n                    dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)\n                }\n            }.show()\n            return true\n        }\n\n        private fun onCopyAccessKeyClick(): Boolean {\n            val manager = context.getSystemService(ClipboardManager::class.java)\n\n            manager.setPrimaryClip(ClipData.newPlainText(\"access_key\", instance.accessKey))\n            Toast.makeText(context, R.string.copy_access_key_toast, Toast.LENGTH_SHORT).show()\n\n            return true\n        }\n\n        private fun onPurifyStoryVideoAdClick(): Boolean {\n            AlertDialog.Builder(activity).apply {\n                val tagMap = linkedMapOf(\n                    \"ad\" to \"广告（推荐勾选）\",\n                    \"short\" to \"短剧（推荐勾选）\",\n                    \"shopping\" to \"购物（推荐勾选）\",\n                    \"tv\" to \"电视剧（强烈不推荐勾选，此处仅为视频同款电视剧，勾选后将无法见带有此标签视频！\",\n                    \"doc\" to \"纪录片（强烈不推荐勾选，此处仅为视频同款纪录片，勾选后将无法见带有此标签视频！）\",\n                    \"ent\" to \"娱乐（强烈不推荐勾选，此处仅为视频同款内容，勾选后将无法见带有此标签视频！）\",\n                    \"movie\" to \"电影（强烈不推荐勾选，此处仅为视频同款电影，勾选后将无法见带有此标签视频！）\",\n                    \"music\" to \"音乐（强烈不推荐勾选，此处仅为视频同款音乐，勾选后将无法看见带有此标签视频！\",\n                    \"topic\" to \"话题（强烈不推荐勾选，此处仅为视频相关话题，勾选后将无法看见带有此标签视频！\",\n                )\n\n                val prefKey = \"purify_story_video_ad_tags\"\n                val oldPurifyAdTags = sPrefs.getStringSet(prefKey, emptySet()) ?: emptySet()\n\n                val keys = tagMap.keys.toList()\n                val values = tagMap.values.toTypedArray()\n\n                val checkedTags = BooleanArray(keys.size) { index ->\n                    keys[index] in oldPurifyAdTags\n                }\n\n                val blockedCount = sPrefs.getInt(\"purify_story_video_ad_blocked_count\", 0)\n\n                setTitle(context.getString(R.string.purify_story_video_ad_title) + \"（累计拦截 $blockedCount 条）\")\n                setPositiveButton(context.getString(android.R.string.ok)) { _, _ ->\n                    val selected = mutableSetOf<String>()\n                    for (i in keys.indices) {\n                        if (checkedTags[i]) selected.add(keys[i])\n                    }\n                    sPrefs.edit().putStringSet(prefKey, selected).apply()\n                    Toast.makeText(activity, \"已保存净化选项，重启客户端生效\", Toast.LENGTH_SHORT).show()\n                }\n\n                setNegativeButton(android.R.string.cancel, null)\n\n                setNeutralButton(\"重置\") { _, _ ->\n                    sPrefs.edit().remove(prefKey).apply()\n                }\n\n                setMultiChoiceItems(values, checkedTags) { _, which, isChecked ->\n                    checkedTags[which] = isChecked\n                }\n            }.show()\n            return true\n        }\n\n        private fun onLongPressSpeedClick(): Boolean {\n            AlertDialog.Builder(activity).run {\n                val view = context.inflateLayout(R.layout.seekbar_dialog)\n                val seekBar = view.findViewById<SeekBar>(R.id.seekBar)\n                seekBar.max = 100\n                val tvHint = view.findViewById<TextView>(R.id.tvHint)\n                seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {\n                    @SuppressLint(\"SetTextI18n\")\n                    override fun onProgressChanged(\n                        seekBar: SeekBar?, progress: Int, fromUser: Boolean\n                    ) {\n                        tvHint.text = \"${progress * 10}%\"\n                    }\n\n                    override fun onStartTrackingTouch(seekBar: SeekBar?) {}\n                    override fun onStopTrackingTouch(seekBar: SeekBar?) {}\n                })\n                val current = prefs.getInt(\"long_press_speed\", 300)\n                @SuppressLint(\"SetTextI18n\")\n                tvHint.text = \"${current * 10}%\"\n                seekBar.progress = current / 10\n                setTitle(R.string.long_press_speed)\n                setNegativeButton(android.R.string.cancel, null)\n                setPositiveButton(android.R.string.ok) { _, _ ->\n                    prefs.edit().putInt(\"long_press_speed\", seekBar.progress * 10).apply()\n                }\n                setView(view)\n                show()\n\n            }\n            return true\n        }\n\n        @Deprecated(\"Deprecated in Java\")\n        override fun onPreferenceClick(preference: Preference) = when (preference.key) {\n            \"version\" -> onVersionClick()\n            \"update\" -> onUpdateClick()\n            \"custom_server\" -> onCustomServerClick()\n            \"test_upos\" -> onTestUposClick()\n            \"customize_bottom_bar\" -> onCustomizeBottomBarClick()\n            \"pref_export\" -> onPrefExportClick()\n            \"pref_import\" -> onPrefImportClick()\n            \"export_video\" -> onExportVideoClick()\n            \"customize_accessKey\" -> onCustomizeAccessKeyClick()\n            \"home_filter\" -> onHomeFilterClick()\n            \"share_log\" -> onShareLogClick()\n            \"customize_drawer\" -> onCustomizeDrawerClick()\n            \"custom_link\" -> onCustomLinkClick()\n            \"customize_dynamic\" -> onCustomDynamicClick()\n            \"danmaku_filter\" -> onDanmakuFilterClick()\n            \"default_speed\" -> onDefaultSpeedClick()\n            \"filter_search\" -> onFilterSearchClick()\n            \"filter_comment\" -> onFilterCommentClick()\n            \"copy_access_key\" -> onCopyAccessKeyClick()\n            \"purify_story_video_ad\" -> onPurifyStoryVideoAdClick()\n            \"long_press_speed\" -> onLongPressSpeedClick()\n            else -> false\n        }\n    }\n\n    class Hint(val hint: String, val startIdx: Int, val fullText: CharSequence)\n    class SearchItem(\n        val preference: Preference,\n        val key: String,\n        private val title: CharSequence,\n        private val summary: CharSequence,\n        private val entries: Array<out CharSequence>,\n        private val isGroup: Boolean,\n        val extra: MutableList<String> = mutableListOf(),\n    ) {\n        var cacheScore = 0\n            private set\n\n        fun calcScoreAndApplyHintBy(text: String): Int {\n            if (text.isEmpty() || isGroup) {\n                cacheScore = 0\n                return 0\n            }\n            var score = 0\n            var titleHint: Hint? = null\n            var summaryHint: Hint? = null\n            var otherHint: Hint? = null\n            if (title.isNotEmpty() && title.indexOf(text, ignoreCase = true).takeIf { it != -1 }\n                    ?.also { titleHint = Hint(text, it, title) } != null\n            ) score += 12\n            if (summary.isNotEmpty() && summary.indexOf(text, ignoreCase = true).takeIf { it != -1 }\n                    ?.also { summaryHint = Hint(text, it, summary) } != null\n            ) score += 6\n            if (entries.isNotEmpty() && entries.firstNotNullOfOrNull { e ->\n                    e.indexOf(text, ignoreCase = true).takeIf { it != -1 }\n                        ?.also { otherHint = Hint(text, it, e) }\n                } != null) {\n                score += 3\n            }\n            if (extra.isNotEmpty() && extra.firstNotNullOfOrNull { e ->\n                    e.indexOf(text, ignoreCase = true).takeIf { it != -1 }\n                        ?.also { if (otherHint == null) otherHint = Hint(text, it, e) }\n                } != null) {\n                score += 2\n            }\n            cacheScore = score\n            applyHint(titleHint, summaryHint, otherHint)\n            return score\n        }\n\n        fun restore() {\n            preference.title = title\n            preference.summary = summary\n        }\n\n        private fun applyHint(titleHint: Hint?, summaryHint: Hint?, otherHint: Hint?) {\n            preference.title = title.withHint(titleHint)\n            if (titleHint == null && summaryHint != null) {\n                preference.summary = summary.withHint(summaryHint)\n            } else if (titleHint == null && otherHint != null) {\n                preference.summary = SpannableStringBuilder(summary).apply {\n                    if (isNotEmpty()) appendLine()\n                    append(otherHint.fullText.withHint(otherHint, true))\n                }\n            } else {\n                preference.summary = summary\n            }\n        }\n\n        private fun CharSequence.withHint(hint: Hint?, other: Boolean = false): CharSequence {\n            if (hint == null || hint.hint.isEmpty())\n                return this\n            val startIdx = hint.startIdx\n            if (startIdx == -1) return this\n            val endIdx = startIdx + hint.hint.length\n            if (endIdx > length) return this\n            val flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n            val hintColor = preference.context.getColor(R.color.text_search_hint)\n            val colorSpan = ForegroundColorSpan(hintColor)\n            val boldSpan = StyleSpan(Typeface.BOLD)\n            return SpannableStringBuilder(this).apply {\n                setSpan(colorSpan, startIdx, endIdx, flags)\n                setSpan(boldSpan, startIdx, endIdx, flags)\n                if (other) {\n                    // to make other text smaller and append to summary\n                    val sizeSpan = AbsoluteSizeSpan(12.sp, false)\n                    setSpan(sizeSpan, 0, length, flags)\n                }\n            }\n        }\n    }\n\n    private fun getContentView(fragment: PrefsFragment): View {\n        val contentView = LinearLayout(fragment.context).apply {\n            orientation = LinearLayout.VERTICAL\n            layoutParams = FrameLayout.LayoutParams(\n                FrameLayout.LayoutParams.MATCH_PARENT,\n                FrameLayout.LayoutParams.MATCH_PARENT\n            )\n        }\n        val searchBar = context.inflateLayout(R.layout.search_bar)\n        val editView = searchBar.findViewById<EditText>(R.id.search)\n        val clearView = searchBar.findViewById<View>(R.id.clear)\n        searchBar.setOnClickListener {\n            editView.requestFocus()\n            context.getSystemService(InputMethodManager::class.java)\n                ?.showSoftInput(editView, 0)\n        }\n        editView.addTextChangedListener(object : TextWatcher {\n            override fun afterTextChanged(s: Editable?) {}\n            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}\n            override fun onTextChanged(\n                s: CharSequence?, start: Int, before: Int, count: Int\n            ) = fragment.search(s?.toString()?.trim().orEmpty())\n        })\n        editView.setOnEditorActionListener { v, actionId, _ ->\n            if (actionId == EditorInfo.IME_ACTION_SEARCH) {\n                fragment.search(v.text.toString().trim())\n                true\n            } else false\n        }\n        clearView.setOnClickListener {\n            editView.setText(\"\")\n        }\n        contentView.addView(searchBar)\n        contentView.addView(fragment.view)\n        return contentView\n    }\n\n    init {\n        val activity = context as Activity\n        activity.addModuleAssets()\n\n        // dirty way to make list preference summary span style take effect,\n        // we have no choice, see ListPreference#getSummary\n        val summaryHook = ListPreference::class.java.hookBeforeMethod(\"getSummary\") { param ->\n            param.thisObject.setObjectField(\"mSummary\", null)\n        }\n\n        val prefsFragment = PrefsFragment()\n        activity.fragmentManager.beginTransaction().add(prefsFragment, \"Setting\").commit()\n        activity.fragmentManager.executePendingTransactions()\n\n        prefsFragment.onActivityCreated(null)\n\n        val unhook = Preference::class.java.hookAfterMethod(\n            \"onCreateView\", ViewGroup::class.java\n        ) { param ->\n            if (PreferenceCategory::class.java.isInstance(param.thisObject)\n                && TextView::class.java.isInstance(param.result)\n            ) {\n                val textView = param.result as TextView\n                if (textView.textColors.defaultColor == -13816531)\n                    textView.setTextColor(Color.GRAY)\n            }\n        }\n\n        setView(getContentView(prefsFragment))\n        setTitle(\"哔哩漫游设置\")\n        setNegativeButton(\"返回\", null)\n        setPositiveButton(\"确定并重启客户端\") { _, _ ->\n            prefsFragment.preferenceManager.forceSavePreference()\n            restartApplication(activity)\n        }\n        setOnDismissListener {\n            unhook?.unhook()\n            summaryHook?.unhook()\n        }\n    }\n\n    companion object {\n        @JvmStatic\n        fun restartApplication(activity: Activity) {\n            // https://stackoverflow.com/a/58530756\n            val pm = activity.packageManager\n            val intent = pm.getLaunchIntentForPackage(activity.packageName)\n            activity.finishAffinity()\n            activity.startActivity(intent)\n            exitProcess(0)\n        }\n\n        @SuppressLint(\"CommitPrefEdits\")\n        @JvmStatic\n        fun PreferenceManager.forceSavePreference() {\n            sharedPreferences.let {\n                val cm = (getObjectFieldOrNull(\"mEditor\")\n                    ?: it.edit()).callMethodOrNull(\"commitToMemory\")\n                val lock = it.getObjectFieldOrNull(\"mWritingToDiskLock\") ?: return@let\n                synchronized(lock) {\n                    it.callMethodOrNull(\"writeToFile\", cm, true)\n                }\n            }\n        }\n\n        fun show(context: Context) {\n            try {\n                SettingDialog(context).show()\n            } catch (e: Resources.NotFoundException) {\n                AlertDialog.Builder(context)\n                    .setTitle(\"需要重启\")\n                    .setMessage(\"哔哩漫游更新了\")\n                    .setPositiveButton(\"重启\") { _, _ ->\n                        restartApplication(context as Activity)\n                    }.show()\n            }\n        }\n\n        const val SPLASH_SELECTION = 0\n        const val LOGO_SELECTION = 1\n        const val PREF_IMPORT = 2\n        const val PREF_EXPORT = 3\n        const val VIDEO_EXPORT = 4\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/SpeedTestDialog.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.net.Uri\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ArrayAdapter\nimport android.widget.ListView\nimport android.widget.TextView\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.asFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.toList\nimport me.iacn.biliroaming.network.BiliRoamingApi\nimport me.iacn.biliroaming.network.BiliRoamingApi.getPlayUrl\nimport me.iacn.biliroaming.network.BiliRoamingApi.mainlandTestParams\nimport me.iacn.biliroaming.network.BiliRoamingApi.overseaTestParams\nimport me.iacn.biliroaming.utils.*\nimport org.json.JSONObject\nimport java.net.URL\nimport java.util.concurrent.Executors\nimport java.util.concurrent.TimeUnit\n\ndata class SpeedTestResult(val name: String, val value: String, var speed: String)\n\nclass SpeedTestAdapter(context: Context) : ArrayAdapter<SpeedTestResult>(context, 0) {\n    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {\n        return (convertView ?: context.inflateLayout(R.layout.cdn_speedtest_item)).apply {\n            getItem(position).let {\n                findViewById<TextView>(R.id.upos_name).text = it?.name\n                findViewById<TextView>(R.id.upos_speed).text =\n                    context.getString(R.string.speed_formatter, it?.speed)\n            }\n        }\n    }\n\n    fun sort() = sort { a, b ->\n        val aSpeed = a.speed.toLongOrNull()\n        val bSpeed = b.speed.toLongOrNull()\n        if (aSpeed == null && bSpeed == null)\n            0\n        else if (aSpeed == null)\n            1\n        else if (bSpeed == null)\n            -1\n        else\n            (bSpeed - aSpeed).toInt()\n    }\n}\n\nclass SpeedTestDialog(activity: Activity, prefs: SharedPreferences) :\n    AlertDialog.Builder(activity) {\n    private val scope = MainScope()\n    private val speedTestDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()\n\n    private val view = ListView(activity)\n    private val adapter = SpeedTestAdapter(activity)\n\n    init {\n        view.adapter = adapter\n\n        view.addHeaderView(context.inflateLayout(R.layout.cdn_speedtest_item).apply {\n            findViewById<TextView>(R.id.upos_name).text = context.getString(R.string.upos)\n            findViewById<TextView>(R.id.upos_speed).text = context.getString(R.string.speed)\n        }, null, false)\n\n        view.setPadding(16.dp, 10.dp, 16.dp, 10.dp)\n\n        setView(view)\n\n        setPositiveButton(\"关闭\", null)\n\n        setOnDismissListener {\n            scope.cancel()\n        }\n\n        view.setOnItemClickListener { _, _, pos, _ ->\n            val (name, value, _) = adapter.getItem(pos - 1/*headerView*/)\n                ?: return@setOnItemClickListener\n            Log.d(\"Use UPOS Server $name: $value\")\n            prefs.edit().putString(\"upos_host\", value).apply()\n            Log.toast(\"已启用 UPOS 服务器：${name}\", force = true)\n        }\n\n        setTitle(\"CDN 测速\")\n    }\n\n    override fun show(): AlertDialog {\n        val dialog = super.show()\n        scope.launch {\n            dialog.setTitle(\"正在测速……\")\n            val url = getTestUrl() ?: run {\n                dialog.setTitle(\"测速失败\")\n                return@launch\n            }\n            context.resources.getStringArray(R.array.upos_entries)\n                .zip(context.resources.getStringArray(R.array.upos_values)).asFlow().map {\n                    scope.launch {\n                        val item = SpeedTestResult(it.first, it.second, \"...\")\n                        adapter.add(item)\n                        adapter.sort()\n                        val speed = speedTest(it.second, url)\n                        item.speed = speed.toString()\n                        adapter.sort()\n                    }\n                }.toList().joinAll()\n            dialog.setTitle(\"测速完成\")\n        }\n        return dialog\n    }\n\n    @Suppress(\"BlockingMethodInNonBlockingContext\") // Fuck JetBrain\n    private suspend fun speedTest(upos: String, rawUrl: String) = try {\n        withContext(speedTestDispatcher) {\n            withTimeout(5000) {\n                val url = if (upos == \"\\$1\") URL(rawUrl) else {\n                    URL(Uri.parse(rawUrl).buildUpon().authority(upos).build().toString())\n                }\n                val connection = url.openConnection()\n                connection.connectTimeout = 5000\n                connection.readTimeout = 5000\n                connection.setRequestProperty(\"User-Agent\", \"Bilibili Freedoooooom/MarkII\")\n                connection.connect()\n                val buffer = ByteArray(2048)\n                var size = 0\n                val start = System.currentTimeMillis()\n                connection.getInputStream().use { stream ->\n                    while (isActive) {\n                        val read = stream.read(buffer)\n                        if (read <= 0) break\n                        size += read\n                    }\n                }\n                size / (System.currentTimeMillis() - start) // KB/s\n            }\n        }\n    } catch (e: Throwable) {\n        0L\n    }\n\n    private suspend fun getTestUrl() = try {\n        withContext(speedTestDispatcher) {\n            withTimeout(5000) {\n                val cn = runCatchingOrNull { XposedInit.country.get(5L, TimeUnit.SECONDS) } == \"cn\"\n                val json = if (cn) {\n                    getPlayUrl(overseaTestParams, arrayOf(\"hk\", \"tw\"))\n                } else getPlayUrl(mainlandTestParams, arrayOf(\"cn\"))\n                json?.toJSONObject()?.optJSONObject(\"dash\")?.getJSONArray(\"audio\")\n                    ?.asSequence<JSONObject>()\n                    ?.minWithOrNull { a, b -> a.optInt(\"bandwidth\") - b.optInt(\"bandwidth\") }\n                    ?.optString(\"base_url\")?.replace(\"https\", \"http\")\n            }\n        }\n    } catch (e: BiliRoamingApi.CustomServerException) {\n        Log.w(\"请求解析服务器发生错误: ${e.message}\")\n        null\n    } catch (e: Throwable) {\n        null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/VideoExportDialog.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.app.Fragment\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.Intent\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ArrayAdapter\nimport android.widget.CheckBox\nimport android.widget.ListView\nimport android.widget.TextView\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.inflateLayout\nimport me.iacn.biliroaming.utils.toJSONObject\nimport java.io.File\n\nclass VideoExportDialog(activity: Activity, fragment: Fragment) : AlertDialog.Builder(activity) {\n    companion object {\n        /**\n         * 指向视频页的文件\n         */\n        var videosToExport = emptySet<File>()\n    }\n\n    private val view = ListView(activity)\n    private val selectedVideos = mutableSetOf<File>()\n\n    init {\n        val allVideos = mutableListOf<VideoEntry>()\n        File(activity.externalCacheDir, \"../download\").listFiles()?.forEach { video ->\n            video.listFiles()?.forEach { page ->\n                try {\n                    val jsonObj = File(page, \"entry.json\").readText().toJSONObject()\n                    val pageData = jsonObj.optJSONObject(\"page_data\")?.let {\n                        VideoEntry.PageData(\n                            it.optString(\"part\")\n                        )\n                    }\n                    val ep = jsonObj.optJSONObject(\"ep\")?.let {\n                        VideoEntry.Ep(\n                            it.optLong(\"av_id\"),\n                            it.optString(\"bvid\"),\n                            it.optString(\"index\"),\n                            it.optString(\"index_title\")\n                        )\n                    }\n                    val videoEntry = VideoEntry(\n                        jsonObj.optString(\"title\"),\n                        jsonObj.optLong(\"avid\"),\n                        jsonObj.optString(\"bvid\"),\n                        pageData, ep\n                    )\n                    videoEntry.path = page\n                    allVideos.add(videoEntry)\n                } catch (e: Throwable) {\n                    Log.toast(\"${e.message}\", true, alsoLog = true)\n                }\n            }\n        }\n        view.adapter = VideoExportAdapter(activity, allVideos, selectedVideos)\n        view.setPadding(50, 20, 50, 20)\n\n        setNegativeButton(\"取消\", null)\n\n        setPositiveButton(\"导出\") { _, _ ->\n            videosToExport = selectedVideos\n            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)\n            try {\n                fragment.startActivityForResult(\n                    Intent.createChooser(intent, \"导出视频\"),\n                    SettingDialog.VIDEO_EXPORT\n                )\n            } catch (ex: ActivityNotFoundException) {\n                Log.toast(\"请安装文件管理器\")\n            }\n        }\n\n        setView(view)\n    }\n\n    class VideoEntry(\n        val title: String,\n        val avid: Long,\n        val bvid: String,\n        val pageData: PageData?,\n        val ep: Ep?,\n        var path: File? = null\n    ) {\n        class PageData(\n            val part: String\n        )\n\n        class Ep(\n            val avid: Long,\n            val bvid: String,\n            val index: String,\n            val indexTitle: String\n        )\n\n        val aBvid\n            get() = ep?.bvid ?: bvid\n        val aid get() = ep?.avid ?: avid\n        val pageTitle get() = pageData?.part ?: ep?.let { \"${it.index} ${it.indexTitle}\" }\n    }\n\n    class VideoExportAdapter(\n        context: Context,\n        private val allVideos: List<VideoEntry>,\n        private val selectedVideos: MutableSet<File>\n    ) : ArrayAdapter<VideoEntry>(context, 0) {\n        @SuppressLint(\"SetTextI18n\")\n        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {\n            return (convertView ?: context.inflateLayout(R.layout.video_choose)).apply {\n                allVideos[position].let {\n                    findViewById<TextView>(R.id.tv_title).text = it.title\n                    findViewById<TextView>(R.id.tv_pageTitle).text = it.pageTitle\n                    findViewById<TextView>(R.id.tv_aid).text = \"av${it.aid}\"\n                    findViewById<TextView>(R.id.tv_bvid).text = it.aBvid\n                    findViewById<CheckBox>(R.id.cb).setOnCheckedChangeListener { _, isChecked ->\n                        if (isChecked)\n                            it.path?.let { it1 -> selectedVideos.add(it1) }\n                        else\n                            selectedVideos.remove(it.path)\n                    }\n                }\n            }\n        }\n\n        override fun getCount() = allVideos.size\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/XposedInit.kt",
    "content": "package me.iacn.biliroaming\n\nimport android.app.Activity\nimport android.app.Application\nimport android.app.Instrumentation\nimport android.content.Context\nimport android.content.res.Resources\nimport android.content.res.XModuleResources\nimport android.os.Build\nimport de.robv.android.xposed.IXposedHookLoadPackage\nimport de.robv.android.xposed.IXposedHookZygoteInit\nimport de.robv.android.xposed.XC_MethodHook\nimport de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.future.future\nimport me.iacn.biliroaming.hook.*\nimport me.iacn.biliroaming.utils.*\nimport java.util.concurrent.CompletableFuture\n\n\n/**\n * Created by iAcn on 2019/3/24\n * Email i@iacn.me\n */\nclass XposedInit : IXposedHookLoadPackage, IXposedHookZygoteInit {\n    override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {\n        modulePath = startupParam.modulePath\n        moduleRes = getModuleRes(modulePath)\n    }\n\n    override fun handleLoadPackage(lpparam: LoadPackageParam) {\n        if (BuildConfig.APPLICATION_ID == lpparam.packageName) {\n            MainActivity.Companion::class.java.name.replaceMethod(\n                lpparam.classLoader,\n                \"isModuleActive\"\n            ) { true }\n            return\n        }\n        if (!Constant.BILIBILI_PACKAGE_NAME.containsValue(lpparam.packageName) &&\n            \"tv.danmaku.bili.MainActivityV2\".findClassOrNull(lpparam.classLoader) == null\n        ) return\n        Instrumentation::class.java.hookBeforeMethod(\n            \"callApplicationOnCreate\",\n            Application::class.java\n        ) { param ->\n            // Hook main process and download process\n            when {\n                !lpparam.processName.contains(\":\") -> {\n                    if (shouldSaveLog) {\n                        startLog()\n                    }\n                    Log.d(\"BiliBili process launched ...\")\n                    Log.d(\"BiliRoaming version: ${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) from $modulePath${if (isBuiltIn) \"(BuiltIn)\" else \"\"}\")\n                    Log.d(\"Bilibili version: ${getPackageVersion(lpparam.packageName)} (${if (is64) \"64\" else \"32\"}bit)\")\n                    Log.d(\"SDK: ${Build.VERSION.RELEASE}(${Build.VERSION.SDK_INT}); Phone: ${Build.BRAND} ${Build.MODEL}\")\n                    Log.d(\"Config: ${sPrefs.all}\")\n                    Log.toast(\n                        \"哔哩漫游已激活${\n                            if (sPrefs.getBoolean(\"main_func\", false) &&\n                                (!sPrefs.getString(\"hk_server\", null).isNullOrEmpty() ||\n                                        !sPrefs.getString(\"th_server\", null).isNullOrEmpty() ||\n                                        !sPrefs.getString(\"tw_server\", null).isNullOrEmpty() ||\n                                        !sPrefs.getString(\"cn_server\", null).isNullOrEmpty())\n                            ) \"\"\n                            else \"。\\n但未启用番剧解锁功能，请检查解析服务器设置。\"\n                        }\\n请勿在B站任何地方宣传漫游。\\n漫游插件开源免费，谨防被骗。\"\n                    )\n\n                    country = MainScope().future(Dispatchers.IO) {\n                        when (fetchJson(Constant.zoneUrl)?.optJSONObject(\"data\")?.optInt(\"country_code\", 0)?.or(0)) {\n                            86 -> \"cn\"\n                            852, 853 -> \"hk\"\n                            886 -> \"tw\"\n                            else -> \"global\"\n                        }.also { Log.d(\"当前地区: $it\") }\n                    }\n\n                    BiliBiliPackage(lpparam.classLoader, param.args[0] as Context)\n                    if (BuildConfig.DEBUG) {\n                        startHook { SSLHook(lpparam.classLoader) }\n                    }\n                    startHook { KillDelayBootHook(lpparam.classLoader) }\n                    startHook { HintHook(lpparam.classLoader) }\n                    startHook { BangumiSeasonHook(lpparam.classLoader) }\n                    startHook { BangumiPlayUrlHook(lpparam.classLoader) }\n                    startHook { PegasusHook(lpparam.classLoader) }\n                    startHook { CustomThemeHook(lpparam.classLoader) }\n                    startHook { TeenagersModeHook(lpparam.classLoader) }\n                    startHook { JsonHook(lpparam.classLoader) }\n                    startHook { ShareHook(lpparam.classLoader) }\n                    startHook { AutoLikeHook(lpparam.classLoader) }\n                    startHook { SettingHook(lpparam.classLoader) }\n                    startHook { SplashHook(lpparam.classLoader) }\n                    startHook { EnvHook(lpparam.classLoader) }\n                    startHook { DownloadThreadHook(lpparam.classLoader) }\n                    startHook { MusicNotificationHook(lpparam.classLoader) }\n                    startHook { DrawerHook(lpparam.classLoader) }\n                    startHook { CoverHook(lpparam.classLoader) }\n                    startHook { SubtitleHook(lpparam.classLoader) }\n                    startHook { CopyHook(lpparam.classLoader) }\n                    startHook { CopyCommentHook(lpparam.classLoader) }\n                    startHook { LiveRoomHook(lpparam.classLoader) }\n                    startHook { QualityHook(lpparam.classLoader) }\n                    startHook { DynamicHook(lpparam.classLoader) }\n                    startHook { ProtoBufHook(lpparam.classLoader) }\n                    startHook { PlayArcConfHook(lpparam.classLoader) }\n                    startHook { TryWatchVipQualityHook(lpparam.classLoader) }\n                    startHook { AllowMiniPlayHook(lpparam.classLoader) }\n                    startHook { StartActivityHook(lpparam.classLoader) }\n                    startHook { FullStoryHook(lpparam.classLoader) }\n                    startHook { DialogBlurBackgroundHook(lpparam.classLoader) }\n                    startHook { PlayerLongPressHook(lpparam.classLoader) }\n                    startHook { BlockUpdateHook(lpparam.classLoader) }\n                    startHook { VipSectionHook(lpparam.classLoader) }\n                    startHook { CommentImageHook(lpparam.classLoader) }\n                    startHook { WebViewHook(lpparam.classLoader) }\n                    startHook { P2pHook(lpparam.classLoader) }\n                    startHook { DanmakuHook(lpparam.classLoader) }\n                    startHook { BangumiPageAdHook(lpparam.classLoader) }\n                    startHook { VideoQualityHook(lpparam.classLoader) }\n                    // startHook(PublishToFollowingHook(lpparam.classLoader))\n                    startHook { UposReplaceHook(lpparam.classLoader) }\n                    startHook { SpeedHook(lpparam.classLoader) }\n                    startHook { MultiWindowHook(lpparam.classLoader) }\n                    startHook { LiveQualityHook(lpparam.classLoader) }\n                    startHook { StoryPlayerAdHook(lpparam.classLoader) }\n                    startHook { LongPressSpeed(lpparam.classLoader) }\n                }\n\n                lpparam.processName.endsWith(\":web\") -> {\n                    BiliBiliPackage(lpparam.classLoader, param.args[0] as Context)\n                    CustomThemeHook(lpparam.classLoader).insertColorForWebProcess()\n                    startHook { WebViewHook(lpparam.classLoader) }\n                    startHook { ShareHook(lpparam.classLoader) }\n                    startHook { DialogBlurBackgroundHook(lpparam.classLoader) }\n                    startHook { RewardAdHook(lpparam.classLoader) }\n                }\n\n                lpparam.processName.endsWith(\":download\") -> {\n                    BiliBiliPackage(lpparam.classLoader, param.args[0] as Context)\n                    startHook { BangumiPlayUrlHook(lpparam.classLoader) }\n                }\n            }\n        }\n        lateInitHook = Activity::class.java.hookBeforeMethod(\"onResume\") {\n            startLateHook()\n            lateInitHook?.unhook()\n        }\n    }\n\n    @Deprecated(\n        \"Use startHook(hookerCreater: () -> BaseHook) instead\",\n        ReplaceWith(\"startHook { hooker }\")\n    )\n    private fun startHook(hooker: BaseHook) {\n        startHook { hooker }\n    }\n\n    private fun startHook(hookerCreater: () -> BaseHook) {\n        try {\n            val hooker = hookerCreater()\n            hookers.add(hooker)\n            hooker.startHook()\n        } catch (e: Throwable) {\n            Log.e(e)\n            Log.toast(\"出现错误\\n${e.message}\\n部分功能可能失效。${e.stackTrace.joinToString(\"\\n\")}\")\n        }\n    }\n\n    private fun startLateHook() {\n        hookers.forEach {\n            try {\n                it.lateInitHook()\n            } catch (e: Throwable) {\n                Log.e(e)\n                Log.toast(\"出现错误\\n${e.message}\\n部分功能可能失效。${e.stackTrace.joinToString(\"\\n\")}\")\n            }\n        }\n    }\n\n    private fun startLog() = try {\n        if (logFile.exists()) {\n            if (oldLogFile.exists()) {\n                oldLogFile.delete()\n            }\n            logFile.renameTo(oldLogFile)\n        }\n        logFile.delete()\n        logFile.createNewFile()\n        Runtime.getRuntime().exec(\n            arrayOf(\n                \"logcat\",\n                \"-T\",\n                \"100\",\n                \"-f\",\n                logFile.absolutePath\n            )\n        )\n    } catch (e: Throwable) {\n        Log.e(e)\n        null\n    }\n\n    companion object {\n        lateinit var modulePath: String\n        lateinit var moduleRes: Resources\n        lateinit var country: CompletableFuture<String>\n\n        private val hookers = ArrayList<BaseHook>()\n        private var lateInitHook: XC_MethodHook.Unhook? = null\n\n\n        @JvmStatic\n        fun getModuleRes(path: String): Resources {\n            return XModuleResources.createInstance(path, null)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/AllowMiniPlayHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.utils.from\nimport me.iacn.biliroaming.utils.getStaticObjectField\nimport me.iacn.biliroaming.utils.hookBeforeAllConstructors\nimport me.iacn.biliroaming.utils.hookBeforeConstructor\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass AllowMiniPlayHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"main_func\", false)) return\n\n        if (sPrefs.getBoolean(\"allow_mini_play\", false)) {\n            val miniPlayerType =\n                    \"com.bilibili.lib.media.resource.PlayConfig\\$PlayConfigType\"\n                            .from(mClassLoader)?.getStaticObjectField(\"MINIPLAYER\")\n            \"com.bilibili.lib.media.resource.PlayConfig\\$PlayMenuConfig\".from(mClassLoader)?.run {\n                hookBeforeConstructor(\n                        Boolean::class.javaPrimitiveType,\n                        \"com.bilibili.lib.media.resource.PlayConfig\\$PlayConfigType\"\n                ) { param ->\n                    val type = param.args[1]\n                    if (type == miniPlayerType)\n                        param.args[0] = true\n                }\n                hookBeforeConstructor(Boolean::class.javaPrimitiveType,\n                        \"com.bilibili.lib.media.resource.PlayConfig\\$PlayConfigType\",\n                        List::class.java\n                ) { param ->\n                    val type = param.args[1]\n                    if (type == miniPlayerType)\n                        param.args[0] = true\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/AutoLikeHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.view.View\nimport android.widget.LinearLayout\nimport de.robv.android.xposed.XC_MethodHook.Unhook\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass AutoLikeHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    private val likedVideos = HashSet<Long>()\n\n    companion object {\n        var detail: Pair<Long, Int>? = null\n    }\n\n    private val likeIds by lazy {\n        arrayOf(\n            \"frame_like\",\n            \"like_layout\"\n        ).map { getId(it) }\n    }\n\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"auto_like\", false)) return\n\n        Log.d(\"startHook: AutoLike\")\n\n        val unhookSet = arrayOf<Set<Unhook>?>(null)\n        unhookSet[0] = instance.kingPositionComponentClass?.hookAfterAllConstructors { param ->\n            val kingPositionComponent = param.thisObject\n            val componentMap =\n                kingPositionComponent.getObjectFieldAs<Map<*, *>>(instance.componentMapField())\n\n            val likeComponentClass = componentMap.keys.filterNotNull().firstNotNullOfOrNull {\n                val componentClass = it.javaClass\n                componentClass.declaredFields.firstOrNull {\n                    it.type == Runnable::class.java\n                } ?: return@firstNotNullOfOrNull null\n                componentClass\n            } ?: return@hookAfterAllConstructors\n\n            val rebindView = likeComponentClass.declaredMethods.firstOrNull {\n                it.returnType == Void.TYPE && it.parameterCount == 5\n                        && it.parameterTypes[0] == likeComponentClass\n                        && it.parameterTypes[4] == LinearLayout::class.java\n            } ?: return@hookAfterAllConstructors\n\n            rebindView.hookAfterMethod { p ->\n                performLike(p.args[4] as View)\n            }\n\n            unhookSet[0]?.forEach { it.unhook() }\n        }\n\n        // 番剧分集切换\n        instance.viewUniteMossClass?.hookBeforeMethod(\n            \"arcRefresh\",\n            \"com.bapis.bilibili.app.viewunite.v1.ArcRefreshReq\",\n            instance.mossResponseHandlerClass\n        ) { param ->\n            val req = param.args[0]\n            val aid = req.callMethodAs(\"getAid\")\n                ?: req.callMethodAs<String?>(\"getBvid\")?.let { bv2av(it) }\n            if (aid == null || aid <= 0L) {\n                return@hookBeforeMethod\n            }\n            param.args[1] = param.args[1].mossResponseHandlerReplaceProxy { reply ->\n                reply ?: return@mossResponseHandlerReplaceProxy null\n                val like = reply.callMethod(\"getReqUser\")?.callMethodAs<Int?>(\"getLike\")\n                    ?: return@mossResponseHandlerReplaceProxy null\n                detail = aid to like\n                null\n            }\n        }\n\n        // 竖屏模式\n        instance.storyAbsControllerClass?.hookAfterMethod(\n            instance.setMDataMethod(),\n            instance.storyDetailClass\n        ) { param ->\n            if (param.thisObject.callMethod(instance.getMPlayerMethod()) == null) {\n                return@hookAfterMethod\n            }\n            val storyDetail = instance.fastJsonClass?.callStaticMethodAs<String>(\n                \"toJSONString\",\n                param.args[0]\n            ).toJSONObject()\n            val playerArgs = storyDetail.optJSONObject(\"player_args\")\n            val aid = playerArgs?.optLong(\"aid\")\n            val reqUser = storyDetail.optJSONObject(\"req_user\")\n            val like = reqUser?.optBoolean(\"like\")\n            if (aid == null || like == null) {\n                return@hookAfterMethod\n            }\n            detail = aid to if (like) 1 else 0\n\n            performLike(param.thisObject as View)\n        }\n    }\n\n    private fun performLike(root: View) {\n        val likeView = likeIds.firstNotNullOfOrNull {\n            root.findViewById(it)\n        }\n        likeView?.post {\n            if (shouldClickLike()) {\n                likeView.callOnClick()\n            }\n        }\n    }\n\n    private fun shouldClickLike(): Boolean {\n        val (aid, like) = detail ?: return false\n        if (likedVideos.contains(aid) || like != 0) {\n            return false\n        }\n        likedVideos.add(aid)\n        return true\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/BangumiPageAdHook.kt",
    "content": "package me.iacn.biliroaming.hook\r\n\r\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\r\nimport me.iacn.biliroaming.utils.*\r\n\r\nclass BangumiPageAdHook(classLoader: ClassLoader) : BaseHook(classLoader) {\r\n    override fun startHook() {\r\n        if (!sPrefs.getBoolean(\"block_view_page_ads\", false)) return\r\n        Log.d(\"startHook: BangumiPageAd\")\r\n\r\n        val oGVActivityVoHooker: Hooker = { param ->\r\n            val args = param.args\r\n            for (i in args.indices) {\r\n                when (val item = args[i]) {\r\n                    is Int -> args[i] = 0\r\n                    is MutableList<*> -> item.clear()\r\n                    else -> args[i] = null\r\n                }\r\n            }\r\n        }\r\n        // activity toast ad\r\n        \"com.bilibili.bangumi.data.page.detail.entity.OGVActivityVo\".from(mClassLoader)\r\n            ?.hookBeforeAllConstructors(oGVActivityVoHooker)\r\n        \"com.bilibili.ship.theseus.ogv.activity.OGVActivityVo\".from(mClassLoader)\r\n            ?.hookBeforeAllConstructors(oGVActivityVoHooker)\r\n\r\n        // mall\r\n        instance.bangumiUniformSeasonActivityEntrance()?.let {\r\n            instance.bangumiUniformSeasonClass?.replaceMethod(it) { null }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/BangumiPlayUrlHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.net.Uri\nimport com.google.protobuf.any\nimport me.iacn.biliroaming.*\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.hook.BangumiSeasonHook.Companion.lastSeasonInfo\nimport me.iacn.biliroaming.network.BiliRoamingApi.CustomServerException\nimport me.iacn.biliroaming.network.BiliRoamingApi.getPlayUrl\nimport me.iacn.biliroaming.network.BiliRoamingApi.getSeason\nimport me.iacn.biliroaming.utils.*\nimport me.iacn.biliroaming.utils.UposReplaceHelper.enableUposReplace\nimport me.iacn.biliroaming.utils.UposReplaceHelper.isPCdnUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.replaceUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.videoUposBackups\nimport org.json.JSONObject\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.util.concurrent.atomic.AtomicBoolean\nimport kotlin.math.abs\n\n/**\n * Created by iAcn on 2019/3/29\n * Email i@iacn.me\n */\nclass BangumiPlayUrlHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    companion object {\n        // DASH, HDR, 4K, DOBLY AUDO, DOBLY VISION, 8K, AV1\n        const val MAX_FNVAL = 16 or 64 or 128 or 256 or 512 or 1024 or 2048\n        const val FAIL_CODE = -404\n        val qnApplied = AtomicBoolean(false)\n        private const val PGC_ANY_MODEL_TYPE_URL =\n            \"type.googleapis.com/bilibili.app.playerunite.pgcanymodel.PGCAnyModel\"\n        private const val UGC_ANY_MODEL_TYPE_URL =\n            \"type.googleapis.com/bilibili.app.playerunite.ugcanymodel.UGCAnyModel\"\n        private val codecMap =\n            mapOf(CodeType.CODE264 to 7, CodeType.CODE265 to 12, CodeType.CODEAV1 to 13)\n        val supportedPlayArcIndices = arrayOf(\n            1, // FILPCONF\n            2, // CASTCONF\n            3, // FEEDBACK\n            4, // SUBTITLE\n            5, // PLAYBACKRATE\n            6, // TIMEUP\n            7, // PLAYBACKMODE\n            8, // SCALEMODE\n            9, // BACKGROUNDPLAY\n            10, // LIKE\n            12, // COIN\n            14, // SHARE\n            15, // SCREENSHOT\n            16, // LOCKSCREEN\n            17, // RECOMMEND\n            18, // PLAYBACKSPEED\n            19, // DEFINITION\n            20, // SELECTIONS\n            21, // NEXT\n            22, // EDITDM\n            23, // SMALLWINDOW\n            25, // OUTERDM\n            26, // INNERDM\n            29, // COLORFILTER\n            34, // RECORDSCREEN\n        )\n    }\n\n    private val defaultQn: Int?\n        get() = instance.playerSettingHelperClass?.callStaticMethodAs<Int>(instance.getDefaultQn())\n\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"main_func\", false)) return\n        Log.d(\"startHook: BangumiPlayUrl\")\n        val blockBangumiPageAds = sPrefs.getBoolean(\"block_view_page_ads\", false)\n        val halfScreenQuality = sPrefs.getString(\"half_screen_quality\", \"0\")?.toInt() ?: 0\n        val fullScreenQuality = sPrefs.getString(\"full_screen_quality\", \"0\")?.toInt() ?: 0\n\n        instance.signQueryName()?.let {\n            instance.libBiliClass?.hookBeforeMethod(it, Map::class.java) { param ->\n                @Suppress(\"UNCHECKED_CAST\")\n                val params = param.args[0] as MutableMap<String, String>\n                if (sPrefs.getBoolean(\"allow_download\", false) &&\n                    params.containsKey(\"ep_id\") && params.containsKey(\"dl\")\n                ) {\n                    if (sPrefs.getBoolean(\"fix_download\", false)) {\n                        params[\"dl_fix\"] = \"1\"\n                        params[\"qn\"] = \"0\"\n                        if (params[\"fnval\"] == \"0\" || params[\"fnval\"] == \"1\")\n                            params[\"fnval\"] = MAX_FNVAL.toString()\n                        params[\"fourk\"] = \"1\"\n                    }\n                    params.remove(\"dl\")\n                }\n            }\n        }\n        instance.retrofitResponseClass?.hookBeforeAllConstructors { param ->\n            val url = getRetrofitUrl(param.args[0]) ?: return@hookBeforeAllConstructors\n            val body = param.args[1] ?: return@hookBeforeAllConstructors\n            val dataField =\n                if (instance.generalResponseClass?.isInstance(body) == true) \"data\" else instance.responseDataField()\n            if (!url.startsWith(\"https://api.bilibili.com/x/tv/playurl\") || !lastSeasonInfo.containsKey(\n                    \"area\"\n                ) || lastSeasonInfo[\"area\"] == \"th\" || body.getIntField(\"code\") != FAIL_CODE\n            ) return@hookBeforeAllConstructors\n            val parsed = Uri.parse(url)\n            val cid = parsed.getQueryParameter(\"cid\")\n            val fnval = parsed.getQueryParameter(\"fnval\")\n            val objectId = parsed.getQueryParameter(\"object_id\")\n            val qn = parsed.getQueryParameter(\"qn\")\n            val params =\n                \"cid=$cid&ep_id=$objectId&fnval=$fnval&fnver=0&fourk=1&platform=android&qn=$qn\"\n            val json = try {\n                lastSeasonInfo[\"area\"]?.let { lastArea ->\n                    getPlayUrl(params, arrayOf(lastArea))\n                }\n            } catch (e: CustomServerException) {\n                Log.toast(\"请求解析服务器发生错误: ${e.message}\", alsoLog = true)\n                return@hookBeforeAllConstructors\n            } ?: run {\n                Log.toast(\"获取播放地址失败\")\n                return@hookBeforeAllConstructors\n            }\n            Log.toast(\"已从代理服务器获取播放地址\\n如加载缓慢或黑屏，可去漫游设置中测速并设置 UPOS\")\n            body.setObjectField(\n                dataField, instance.fastJsonClass?.callStaticMethod(\n                    instance.fastJsonParse(),\n                    json,\n                    instance.projectionPlayUrlClass\n                )\n            )\n            body.setIntField(\"code\", 0)\n        }\n\n        \"com.bapis.bilibili.pgc.gateway.player.v1.PlayURLMoss\".findClassOrNull(mClassLoader)?.run {\n            var isDownload = false\n            hookBeforeMethod(\n                \"playView\",\n                \"com.bapis.bilibili.pgc.gateway.player.v1.PlayViewReq\"\n            ) { param ->\n                val request = param.args[0]\n                isDownload = sPrefs.getBoolean(\"allow_download\", false)\n                        && request.callMethodAs<Int>(\"getDownload\") >= 1\n                if (isDownload) {\n                    if (!sPrefs.getBoolean(\"fix_download\", false)\n                        || request.callMethodAs<Int>(\"getFnval\") <= 1\n                    ) {\n                        request.callMethod(\"setFnval\", MAX_FNVAL)\n                        request.callMethod(\"setFourk\", true)\n                    }\n                    request.callMethod(\"setDownload\", 0)\n                } else if (halfScreenQuality == 1 || fullScreenQuality != 0) {\n                    request.callMethod(\"setFnval\", MAX_FNVAL)\n                    request.callMethod(\"setFourk\", true)\n                    if (halfScreenQuality == 1 && qnApplied.compareAndSet(false, true)) {\n                        defaultQn?.let { request.callMethod(\"setQn\", it) }\n                    }\n                }\n            }\n            hookAfterMethod(\n                \"playView\",\n                \"com.bapis.bilibili.pgc.gateway.player.v1.PlayViewReq\"\n            ) { param ->\n                val request = param.args[0]\n                val response = param.result\n                if (!response.callMethodAs<Boolean>(\"hasVideoInfo\")\n                    || needForceProxy(response)\n                ) {\n                    try {\n                        val serializedRequest = request.callMethodAs<ByteArray>(\"toByteArray\")\n                        val req = PlayViewReq.parseFrom(serializedRequest)\n                        val seasonId = req.seasonId.toString().takeIf { it != \"0\" }\n                            ?: lastSeasonInfo[\"season_id\"] ?: \"0\"\n                        val (thaiSeason, thaiEp) = getSeasonLazy(seasonId, req.epId)\n                        val content = getPlayUrl(reconstructQuery(req, response, thaiEp))\n                        content?.let {\n                            Log.toast(\"已从代理服务器获取播放地址\\n如加载缓慢或黑屏，可去漫游设置中测速并设置 UPOS\")\n                            param.result = reconstructResponse(\n                                req, response, it, isDownload, thaiSeason, thaiEp\n                            )\n                        } ?: run {\n                            Log.toast(\"获取播放地址失败\", alsoLog = true)\n                        }\n                    } catch (e: CustomServerException) {\n                        param.result = showPlayerError(\n                            response,\n                            \"请求解析服务器发生错误(点此查看更多)\\n${e.message}\"\n                        )\n                        Log.toast(\"请求解析服务器发生错误: ${e.message}\", alsoLog = true)\n                    }\n                } else if (isDownload) {\n                    param.result = fixDownloadProto(response)\n                } else if (blockBangumiPageAds) {\n                    param.result = purifyViewInfo(response)\n                }\n            }\n        }\n        \"com.bapis.bilibili.pgc.gateway.player.v2.PlayURLMoss\".findClassOrNull(mClassLoader)?.run {\n            var isDownload = false\n            hookBeforeMethod(\n                if (instance.useNewMossFunc) \"executePlayView\" else \"playView\",\n                \"com.bapis.bilibili.pgc.gateway.player.v2.PlayViewReq\"\n            ) { param ->\n                val request = param.args[0]\n                // if getDownload == 1 -> flv download\n                // if getDownload == 2 -> dash download\n                // if qn == 0, we are querying available quality\n                // else we are downloading\n                // if fnval == 0 -> flv download\n                // thus fix download will set qn = 0 and set fnval to max\n                isDownload = sPrefs.getBoolean(\"allow_download\", false)\n                        && request.callMethodAs<Int>(\"getDownload\") >= 1\n                if (isDownload) {\n                    if (!sPrefs.getBoolean(\"fix_download\", false)\n                        || request.callMethodAs<Int>(\"getFnval\") <= 1\n                    ) {\n                        request.callMethod(\"setFnval\", MAX_FNVAL)\n                        request.callMethod(\"setFourk\", true)\n                    }\n                    request.callMethod(\"setDownload\", 0)\n                } else if (halfScreenQuality == 1 || fullScreenQuality != 0) {\n                    request.callMethod(\"setFnval\", MAX_FNVAL)\n                    request.callMethod(\"setFourk\", true)\n                    if (halfScreenQuality == 1 && qnApplied.compareAndSet(false, true)) {\n                        defaultQn?.let { request.callMethod(\"setQn\", it) }\n                    }\n                }\n            }\n            hookAfterMethod(\n                if (instance.useNewMossFunc) \"executePlayView\" else \"playView\",\n                \"com.bapis.bilibili.pgc.gateway.player.v2.PlayViewReq\"\n            ) { param ->\n                // th:\n                // com.bilibili.lib.moss.api.BusinessException: 抱歉您所使用的平台不可观看！\n                // com.bilibili.lib.moss.api.BusinessException: 啥都木有\n                // connection err <- should skip because of cache:\n                // throwable: com.bilibili.lib.moss.api.NetworkException\n                if (instance.networkExceptionClass?.isInstance(param.throwable) == true)\n                    return@hookAfterMethod\n                val request = param.args[0]\n                val response =\n                    param.result ?: \"com.bapis.bilibili.pgc.gateway.player.v2.PlayViewReply\"\n                        .on(mClassLoader).new()\n                if (needProxy(response)) {\n                    try {\n                        val serializedRequest = request.callMethodAs<ByteArray>(\"toByteArray\")\n                        val req = PlayViewReq.parseFrom(serializedRequest)\n                        val seasonId = req.seasonId.toString().takeIf { it != \"0\" }\n                            ?: lastSeasonInfo[\"season_id\"] ?: \"0\"\n                        val (thaiSeason, thaiEp) = getSeasonLazy(seasonId, req.epId)\n                        val content = getPlayUrl(reconstructQuery(req, response, thaiEp))\n                        content?.let {\n                            Log.toast(\"已从代理服务器获取播放地址\\n如加载缓慢或黑屏，可去漫游设置中测速并设置 UPOS\")\n                            param.result = reconstructResponse(\n                                req, response, it, isDownload, thaiSeason, thaiEp\n                            )\n                        }\n                            ?: throw CustomServerException(mapOf(\"未知错误\" to \"请检查哔哩漫游设置中解析服务器设置。\"))\n                    } catch (e: CustomServerException) {\n                        param.result = showPlayerError(\n                            response,\n                            \"请求解析服务器发生错误(点此查看更多)\\n${e.message}\"\n                        )\n                        Log.toast(\"请求解析服务器发生错误: ${e.message}\", alsoLog = true)\n                    }\n                } else if (isDownload) {\n                    param.result = fixDownloadProto(response)\n                } else if (blockBangumiPageAds) {\n                    param.result = purifyViewInfo(response)\n                }\n            }\n        }\n\n        // 修复下载时提示获取剧集信息失败\n        instance.resolveClientCompanionClass?.hookAfterMethod(\n            instance.buildCommonResolverParamsMethod(),\n            instance.videoDownloadEntryClass\n        ) { param ->\n            val entry = param.args[0].callMethodAs<JSONObject?>(\"toJsonObject\")\n            val seasonId = entry?.optString(\"season_id\")\n            val epId = entry?.optJSONObject(\"ep\")?.optString(\"episode_id\")\n            val extraMap = buildMap {\n                seasonId?.let { put(\"season_id\", it) }\n                epId?.let { put(\"ep_id\", it) }\n            }.ifEmpty {\n                return@hookAfterMethod\n            }\n            param.result.callMethod(instance.setExtraContentMethod(), extraMap)\n        }\n\n        instance.playerMossClass?.run {\n            var isDownload = false\n            hookBeforeMethod(\n                if (instance.useNewMossFunc) \"executePlayViewUnite\" else \"playViewUnite\",\n                instance.playViewUniteReqClass\n            ) { param ->\n                val request = param.args[0]\n                val vod = request.callMethod(\"getVod\") ?: return@hookBeforeMethod\n                isDownload = sPrefs.getBoolean(\"allow_download\", false)\n                        && vod.callMethodAs<Int>(\"getDownload\") >= 1\n                if (isDownload) {\n                    if (!sPrefs.getBoolean(\"fix_download\", false)\n                        || vod.callMethodAs<Int>(\"getFnval\") <= 1\n                    ) {\n                        vod.callMethod(\"setFnval\", MAX_FNVAL)\n                        vod.callMethod(\"setFourk\", true)\n                    }\n                    vod.callMethod(\"setDownload\", 0)\n                } else if (halfScreenQuality != 0 || fullScreenQuality != 0) {\n                    // unlock available quality limit, allow quality up to 8K\n                    vod.callMethod(\"setFnval\", MAX_FNVAL)\n                    vod.callMethod(\"setFourk\", true)\n                    if (halfScreenQuality != 0 && qnApplied.compareAndSet(false, true)) {\n                        if (halfScreenQuality != 1) {\n                            vod.callMethod(\"setQn\", halfScreenQuality)\n                        } else {\n                            // follow full screen quality\n                            defaultQn?.let { vod.callMethod(\"setQn\", it) }\n                        }\n                    }\n                }\n            }\n            hookAfterMethod(\n                if (instance.useNewMossFunc) \"executePlayViewUnite\" else \"playViewUnite\",\n                instance.playViewUniteReqClass\n            ) { param ->\n                if (instance.networkExceptionClass?.isInstance(param.throwable) == true)\n                    return@hookAfterMethod\n                val request = param.args[0]\n                val response =\n                    param.result ?: \"com.bapis.bilibili.app.playerunite.v1.PlayViewUniteReply\"\n                        .on(mClassLoader).new()\n                val supplementAny = response.callMethod(\"getSupplement\")\n                val typeUrl = supplementAny?.callMethodAs<String>(\"getTypeUrl\")\n\n                // Only handle pgc video\n                ///////////////////\n                // newPlayUnite:\n                // request.vod.cid, response.play_arc.cid need skip\n                val requestVod = request.callMethod(\"getVod\")\n                val reqCid = requestVod?.callMethodAs<Long>(\"getCid\")\n\n                val responsePlayArc = response.callMethod(\"getPlayArc\")\n                val respCid = responsePlayArc?.callMethodAs<Long>(\"getCid\")\n\n                val isThai = reqCid != 0.toLong() && reqCid != respCid\n                // if it is download request\n                // can't skip\n                if (!isDownload && param.result != null && typeUrl != PGC_ANY_MODEL_TYPE_URL && !isThai\n                ) {\n                    return@hookAfterMethod\n                }\n\n                val extraContent = request.callMethodAs<Map<String, String>>(\"getExtraContentMap\")\n                val seasonId = extraContent.getOrDefault(\"season_id\", \"0\")\n                val reqEpId = extraContent.getOrDefault(\"ep_id\", \"0\").toLong()\n                if (!isDownload && seasonId == \"0\" && reqEpId == 0L)\n                    return@hookAfterMethod\n                val supplement = supplementAny?.callMethod(\"getValue\")\n                    ?.callMethodAs<ByteArray>(\"toByteArray\")\n                    ?.runCatchingOrNull { PlayViewReply.parseFrom(this) } ?: playViewReply {}\n                if (needProxyUnite(response, supplement) || isThai) {\n                    try {\n                        val serializedRequest = request.callMethodAs<ByteArray>(\"toByteArray\")\n                        val req = PlayViewUniteReq.parseFrom(serializedRequest)\n                        val (thaiSeason, thaiEp) = getSeasonLazy(seasonId, reqEpId)\n                        val content = getPlayUrl(reconstructQueryUnite(req, supplement, thaiEp))\n                        content?.let {\n                            Log.toast(\"已从代理服务器获取播放地址\\n如加载缓慢或黑屏，可去漫游设置中测速并设置 UPOS\")\n                            param.result = reconstructResponseUnite(\n                                req, response, supplement, it, isDownload, thaiSeason, thaiEp\n                            )\n                        }\n                            ?: throw CustomServerException(mapOf(\"未知错误\" to \"请检查哔哩漫游设置中解析服务器设置。\"))\n                    } catch (e: CustomServerException) {\n                        // v8.48.0+ 打开缓存弹窗导致应用崩溃\n//                        param.result = showPlayerErrorUnite(\n//                            response, supplement,\n//                            \"请求解析服务器发生错误\", e.message, true\n//                        )\n                        Log.toast(\"请求解析服务器发生错误: ${e.message}\", alsoLog = true)\n                    }\n                } else if (isDownload) {\n                    param.result = fixDownloadProtoUnite(response)\n                } else if (blockBangumiPageAds) {\n                    param.result = purifyViewInfo(response, supplement)\n                }\n            }\n            // 7.41.0+ use async\n            hookBeforeMethod(\n                \"playViewUnite\",\n                instance.playViewUniteReqClass,\n                instance.mossResponseHandlerClass\n            ) { param ->\n                param.args[0].callMethod(\"getVod\")?.apply {\n                    isDownload = sPrefs.getBoolean(\"allow_download\", false)\n                            && callMethodAs<Int>(\"getDownload\") >= 1\n                    if (isDownload) {\n                        if (!sPrefs.getBoolean(\"fix_download\", false)\n                            || callMethodAs<Int>(\"getFnval\") <= 1\n                        ) {\n                            callMethod(\"setFnval\", MAX_FNVAL)\n                            callMethod(\"setFourk\", true)\n                        }\n                        callMethod(\"setDownload\", 0)\n                    } else if (halfScreenQuality != 0 || fullScreenQuality != 0) {\n                        // unlock available quality limit, allow quality up to 8K\n                        callMethod(\"setFnval\", MAX_FNVAL)\n                        callMethod(\"setFourk\", true)\n                        if (halfScreenQuality != 0 && qnApplied.compareAndSet(false, true)) {\n                            if (halfScreenQuality != 1) {\n                                callMethod(\"setQn\", halfScreenQuality)\n                            } else {\n                                // follow full screen quality\n                                defaultQn?.let { callMethod(\"setQn\", it) }\n                            }\n                        }\n                    }\n                }\n                param.args[1] = param.args[1].mossResponseHandlerReplaceProxy { originalResp ->\n                    val request = param.args[0]\n                    val response =\n                        originalResp ?: \"com.bapis.bilibili.app.playerunite.v1.PlayViewUniteReply\"\n                            .on(mClassLoader).new()\n                    val supplementAny = response.callMethod(\"getSupplement\")\n                    val typeUrl = supplementAny?.callMethodAs<String>(\"getTypeUrl\")\n\n                    // Only handle pgc video\n                    ///////////////////\n                    // newPlayUnite:\n                    // request.vod.cid, response.play_arc.cid\n                    val requestVod = request.callMethod(\"getVod\")\n                    val reqCid = requestVod?.callMethodAs<Long>(\"getCid\")\n\n                    val responsePlayArc = response.callMethod(\"getPlayArc\")\n                    val respCid = responsePlayArc?.callMethodAs<Long>(\"getCid\")\n\n                    val isThai = reqCid != 0.toLong() && reqCid != respCid\n                    if (originalResp != null && typeUrl != PGC_ANY_MODEL_TYPE_URL && !isThai) {\n                        return@mossResponseHandlerReplaceProxy null\n                    }\n\n                    val extraContent =\n                        request.callMethodAs<Map<String, String>>(\"getExtraContentMap\")\n                    val seasonId = extraContent.getOrDefault(\"season_id\", \"0\")\n                    val reqEpId = extraContent.getOrDefault(\"ep_id\", \"0\").toLong()\n                    if (seasonId == \"0\" && reqEpId == 0L)\n                        return@mossResponseHandlerReplaceProxy null\n                    val supplement = supplementAny?.callMethod(\"getValue\")\n                        ?.callMethodAs<ByteArray>(\"toByteArray\")\n                        ?.runCatchingOrNull { PlayViewReply.parseFrom(this) } ?: playViewReply {}\n                    val newResponse = if (needProxyUnite(response, supplement) || isThai) {\n                        try {\n                            val serializedRequest = request.callMethodAs<ByteArray>(\"toByteArray\")\n                            val req = PlayViewUniteReq.parseFrom(serializedRequest)\n                            val (thaiSeason, thaiEp) = getSeasonLazy(seasonId, reqEpId)\n                            val content = getPlayUrl(reconstructQueryUnite(req, supplement, thaiEp))\n                            content?.let {\n                                Log.toast(\"已从代理服务器获取播放地址\\n如加载缓慢或黑屏，可去漫游设置中测速并设置 UPOS\")\n                                reconstructResponseUnite(\n                                    req, response, supplement, it, isDownload, thaiSeason, thaiEp\n                                )\n                            }\n                                ?: throw CustomServerException(mapOf(\"未知错误\" to \"请检查哔哩漫游设置中解析服务器设置。\"))\n                        } catch (e: CustomServerException) {\n                            Log.toast(\"请求解析服务器发生错误: ${e.message}\", alsoLog = true)\n                            showPlayerErrorUnite(\n                                response, supplement, \"请求解析服务器发生错误\", e.message\n                            )\n                        }\n                    } else if (isDownload) {\n                        fixDownloadProtoUnite(response)\n                    } else if (blockBangumiPageAds) {\n                        purifyViewInfo(response, supplement)\n                    } else null\n                    newResponse\n                }\n            }\n        }\n        instance.playURLMossClass?.hookBeforeMethod(\n            if (instance.useNewMossFunc) \"executePlayView\" else \"playView\",\n            instance.playViewReqClass\n        ) { param ->\n            val request = param.args[0]\n            val isDownload = request.callMethodAs<Int>(\"getDownload\") >= 1\n            if (isDownload) return@hookBeforeMethod\n            if (halfScreenQuality != 0 || fullScreenQuality != 0) {\n                request.callMethod(\"setFnval\", MAX_FNVAL)\n                request.callMethod(\"setFourk\", true)\n                if (halfScreenQuality != 0 && qnApplied.compareAndSet(false, true)) {\n                    if (halfScreenQuality != 1) {\n                        request.callMethod(\"setQn\", halfScreenQuality)\n                    } else {\n                        defaultQn?.let { request.callMethod(\"setQn\", it) }\n                    }\n                }\n            }\n        }\n    }\n\n    private fun getSeasonLazy(\n        seasonId: String, reqEpId: Long\n    ): Pair<Lazy<JSONObject>, Lazy<JSONObject>> {\n        val season = lazy {\n            getSeason(mapOf(\"season_id\" to seasonId, \"ep_id\" to reqEpId.toString()), null)\n                ?.toJSONObject()?.optJSONObject(\"result\")\n                ?: throw CustomServerException(mapOf(\"解析服务器错误\" to \"无法获取剧集信息\"))\n        }\n        val ep = lazy {\n            season.value.let { s ->\n                s.optJSONArray(\"modules\").orEmpty().asSequence<JSONObject>().flatMap {\n                    it.optJSONObject(\"data\")?.optJSONArray(\"episodes\")\n                        .orEmpty().asSequence<JSONObject>()\n                }.let { es ->\n                    es.firstOrNull { if (reqEpId != 0L) it.optLong(\"id\") == reqEpId else true }\n                } ?: s.optJSONObject(\"new_ep\")?.apply { put(\"status\", 2L) }\n            } ?: throw CustomServerException(mapOf(\"解析服务器错误\" to \"无法获取剧集信息\"))\n        }\n        return season to ep\n    }\n\n    private fun needProxy(response: Any): Boolean {\n        if (!response.callMethodAs<Boolean>(\"hasVideoInfo\")) return true\n\n        val viewInfo = response.callMethod(\"getViewInfo\")\n\n        if (viewInfo?.callMethod(\"getDialog\")\n                ?.callMethodAs<String>(\"getType\") == \"area_limit\"\n        ) return true\n\n        if (viewInfo?.callMethod(\"getEndPage\")?.callMethod(\"getDialog\")\n                ?.callMethodAs<String>(\"getType\") == \"area_limit\"\n        ) return true\n\n        sPrefs.getString(\"cn_server_accessKey\", null) ?: return false\n        val business = response.callMethod(\"getBusiness\")\n        if (business?.callMethodAs<Boolean>(\"getIsPreview\") == true) return true\n        if (viewInfo?.callMethod(\"getDialog\")\n                ?.callMethodAs<String>(\"getType\")?.let { it != \"\" } == true\n        ) return true\n        return viewInfo?.callMethod(\"getEndPage\")?.callMethod(\"getDialog\")\n            ?.callMethodAs<String>(\"getType\")?.let { it != \"\" } == true\n    }\n\n    private fun needProxyUnite(response: Any, supplement: PlayViewReply): Boolean {\n        if (!response.callMethodAs<Boolean>(\"hasVodInfo\")) return true\n\n        val viewInfo = supplement.viewInfo\n        if (viewInfo.dialog.type == \"area_limit\") return true\n        if (viewInfo.endPage.dialog.type == \"area_limit\") return true\n\n        sPrefs.getString(\"cn_server_accessKey\", null) ?: return false\n        if (supplement.business.isPreview) return true\n        if (viewInfo.dialog.type.isNotEmpty()) return true\n        return viewInfo.endPage.dialog.type.isNotEmpty()\n    }\n\n    private fun PlayViewReply.toErrorReply(message: String) = copy {\n        viewInfo = viewInfo.copy {\n            if (endPage.hasDialog()) {\n                dialog = endPage.dialog\n            }\n            dialog = dialog.copy {\n                msg = \"获取播放地址失败\"\n                title = title.copy {\n                    text = message\n                    if (!hasTextColor()) textColor = \"#ffffff\"\n                }\n                image = image.copy {\n                    url =\n                        \"https://i0.hdslb.com/bfs/album/08d5ce2fef8da8adf91024db4a69919b8d02fd5c.png\"\n                }\n                if (!hasCode()) code = 6002003\n                if (!hasStyle()) style = \"horizontal_image\"\n                if (!hasType()) type = \"area_limit\"\n            }\n            clearEndPage()\n        }\n        clearVideoInfo()\n    }\n\n    private fun PlaysharedViewInfo.toErrorReply(message: String, subMessage: String) = copy {\n        val startPlayingDialog = dialogMap[\"start_playing\"] ?: playsharedDialog {}\n        dialogMap.put(\"start_playing\", startPlayingDialog.copy {\n            backgroundInfo = backgroundInfo.copy {\n                drawableBitmapUrl =\n                    \"http://i0.hdslb.com/bfs/bangumi/e42bfa7427456c03562a64ac747be55203e24993.png\"\n                effects = 2 // Effects::HALF_ALPHA\n            }\n            title = title.copy {\n                text = message\n                if (!hasTextColor()) textColor = \"#ffffff\"\n            }\n            subtitle = subtitle.copy {\n                text = subMessage\n                if (!hasTextColor()) textColor = \"#ffffff\"\n            }\n            // use GuideStyle::VERTICAL_TEXT, for HORIZONTAL_IMAGE cannot show error details\n            styleType = 2\n            limitActionType = 1 // SHOW_LIMIT_DIALOG\n        })\n    }\n\n    private fun showPlayerError(response: Any, message: String) = runCatchingOrNull {\n        val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n        val newRes = PlayViewReply.parseFrom(serializedResponse).toErrorReply(message)\n        response.javaClass.callStaticMethod(\"parseFrom\", newRes.toByteArray())\n    } ?: response\n\n    private fun showPlayerErrorUnite(\n        response: Any,\n        supplement: PlayViewReply,\n        message: String,\n        subMessage: String,\n        isBlockingReq: Boolean = false\n    ) =\n        runCatchingOrNull {\n            val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n            val newRes = PlayViewUniteReply.parseFrom(serializedResponse).copy {\n                this.supplement = any {\n                    val supplementMessage = if (isBlockingReq) {\n                        message\n                    } else {\n                        message + \"\\n\" + subMessage\n                    }\n                    typeUrl = PGC_ANY_MODEL_TYPE_URL\n                    value = supplement.toErrorReply(supplementMessage).toByteString()\n                }\n                viewInfo = viewInfo.toErrorReply(message, subMessage)\n                clearVodInfo()\n            }.toByteArray()\n            response.javaClass.callStaticMethod(\"parseFrom\", newRes)\n        } ?: response\n\n    private fun VideoInfoKt.Dsl.fixDownloadProto(checkBaseUrl: Boolean = false) {\n        var audioId = 0\n        var setted = false\n        val checkConnection = fun(url: String) = runCatchingOrNull {\n            val connection = URL(url).openConnection() as HttpURLConnection\n            connection.requestMethod = \"HEAD\"\n            connection.connectTimeout = 1000\n            connection.readTimeout = 1000\n            connection.connect()\n            connection.responseCode == HttpURLConnection.HTTP_OK\n        } ?: false\n        val streams = streamList.map { s ->\n            if (s.streamInfo.quality != quality || setted) {\n                s.copy { clearContent() }\n            } else {\n                audioId = s.dashVideo.audioId\n                setted = true\n                if (checkBaseUrl) {\n                    s.copy {\n                        dashVideo = dashVideo.copy {\n                            if (!checkConnection(baseUrl))\n                                backupUrl.find { checkConnection(it) }?.let {\n                                    baseUrl = it\n                                }\n                        }\n                    }\n                } else s\n            }\n        }\n        val audio = (dashAudio.find {\n            it.id == audioId\n        } ?: dashAudio.first()).let { a ->\n            if (checkBaseUrl) {\n                a.copy {\n                    if (!checkConnection(baseUrl))\n                        backupUrl.find { checkConnection(it) }?.let {\n                            baseUrl = it\n                        }\n                }\n            } else a\n        }\n        streamList.clear()\n        dashAudio.clear()\n        streamList += streams\n        dashAudio += audio\n    }\n\n    private fun fixDownloadProto(response: Any) = runCatchingOrNull {\n        val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n        val newRes = PlayViewReply.parseFrom(serializedResponse).copy {\n            videoInfo = videoInfo.copy { fixDownloadProto() }\n        }.toByteArray()\n        response.javaClass.callStaticMethod(\"parseFrom\", newRes)\n    } ?: response\n\n    private fun fixDownloadProtoUnite(response: Any) = runCatchingOrNull {\n        val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n        val newRes = PlayViewUniteReply.parseFrom(serializedResponse).copy {\n            vodInfo = vodInfo.copy { fixDownloadProto() }\n        }.toByteArray()\n        response.javaClass.callStaticMethod(\"parseFrom\", newRes)\n    } ?: response\n\n    private fun needForceProxy(response: Any): Boolean {\n        sPrefs.getString(\"cn_server_accessKey\", null) ?: return false\n        val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n        return PlayViewReply.parseFrom(serializedResponse).business.isPreview\n    }\n\n    private fun reconstructQuery(\n        req: PlayViewReq,\n        response: Any,\n        thaiEp: Lazy<JSONObject>\n    ): String? {\n        val episodeInfo by lazy {\n            response.callMethodOrNull(\"getBusiness\")?.callMethodOrNull(\"getEpisodeInfo\")\n        }\n        // CANNOT use reflection for compatibility with Xpatch\n        return Uri.Builder().run {\n            appendQueryParameter(\"ep_id\", req.epId.let {\n                if (it != 0L) it else episodeInfo?.callMethodOrNullAs<Int>(\"getEpId\") ?: 0\n            }.let {\n                if (it != 0) it else thaiEp.value.optLong(\"id\")\n            }.toString())\n            appendQueryParameter(\"cid\", req.cid.let {\n                if (it != 0L) it else episodeInfo?.callMethodOrNullAs<Long>(\"getCid\") ?: 0\n            }.let {\n                if (it != 0L) it else thaiEp.value.optLong(\"id\")\n            }.toString())\n            appendQueryParameter(\"qn\", req.qn.toString())\n            appendQueryParameter(\"fnver\", req.fnver.toString())\n            appendQueryParameter(\"fnval\", req.fnval.toString())\n            appendQueryParameter(\"force_host\", req.forceHost.toString())\n            appendQueryParameter(\"fourk\", if (req.fourk) \"1\" else \"0\")\n            build()\n        }.query\n    }\n\n    private fun reconstructQueryUnite(\n        req: PlayViewUniteReq,\n        supplement: PlayViewReply,\n        thaiEp: Lazy<JSONObject>\n    ): String? {\n        val episodeInfo = supplement.business.episodeInfo\n        // CANNOT use reflection for compatibility with Xpatch\n        return Uri.Builder().run {\n            appendQueryParameter(\"ep_id\", req.extraContentMap[\"ep_id\"].let {\n                if (!it.isNullOrEmpty() && it != \"0\") it.toLong() else episodeInfo.epId\n            }.let {\n                if (it != 0) it else thaiEp.value.optLong(\"id\")\n            }.toString())\n            appendQueryParameter(\"cid\", req.vod.cid.let {\n                if (it != 0L) it else episodeInfo.cid\n            }.let {\n                if (it != 0L) it else thaiEp.value.optLong(\"id\")\n            }.toString())\n            appendQueryParameter(\"qn\", req.vod.qn.toString())\n            appendQueryParameter(\"fnver\", req.vod.fnver.toString())\n            appendQueryParameter(\"fnval\", req.vod.fnval.toString())\n            appendQueryParameter(\"force_host\", req.vod.forceHost.toString())\n            appendQueryParameter(\"fourk\", if (req.vod.fourk) \"1\" else \"0\")\n            build()\n        }.query\n    }\n\n    private fun reconstructResponse(\n        req: PlayViewReq,\n        response: Any,\n        content: String,\n        isDownload: Boolean,\n        thaiSeason: Lazy<JSONObject>,\n        thaiEp: Lazy<JSONObject>\n    ) = runCatching {\n        var jsonContent = content.toJSONObject()\n        if (jsonContent.has(\"result\")) {\n            // For kghost server\n            val result = jsonContent.opt(\"result\")\n            if (result != null && result !is String) {\n                jsonContent = jsonContent.getJSONObject(\"result\")\n            }\n        }\n        val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n        val newRes = PlayViewReply.parseFrom(serializedResponse).copy {\n            playConf = playAbilityConf {\n                dislikeDisable = true\n                likeDisable = true\n                elecDisable = true\n                freyaEnterDisable = true\n                freyaFullDisable = true\n            }\n            videoInfo = jsonContent.toVideoInfo(req.preferCodecType, isDownload)\n            fixBusinessProto(thaiSeason, thaiEp, jsonContent)\n            viewInfo = viewInfo {}\n        }.toByteArray()\n        response.javaClass.callStaticMethod(\"parseFrom\", newRes)\n    }.onFailure { Log.e(it) }.getOrDefault(response)\n\n    private fun reconstructResponseUnite(\n        req: PlayViewUniteReq,\n        response: Any,\n        supplement: PlayViewReply,\n        content: String,\n        isDownload: Boolean,\n        thaiSeason: Lazy<JSONObject>,\n        thaiEp: Lazy<JSONObject>\n    ) = runCatching {\n        var jsonContent = content.toJSONObject()\n        if (jsonContent.has(\"result\")) {\n            // For kghost server\n            val result = jsonContent.opt(\"result\")\n            if (result != null && result !is String) {\n                jsonContent = jsonContent.getJSONObject(\"result\")\n            }\n        }\n        val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n        val newRes = PlayViewUniteReply.parseFrom(serializedResponse).copy {\n            vodInfo = jsonContent.toVideoInfo(req.vod.preferCodeType, isDownload)\n            val newSupplement = supplement.copy {\n                fixBusinessProto(thaiSeason, thaiEp, jsonContent)\n                viewInfo = viewInfo {}\n            }\n            this.supplement = any {\n                typeUrl = PGC_ANY_MODEL_TYPE_URL\n                value = newSupplement.toByteString()\n            }\n            playArcConf = playArcConf {\n                val supportedConf = arcConf { isSupport = true }\n                supportedPlayArcIndices.forEach { arcConf[it] = supportedConf }\n            }\n            if (!hasPlayArc()) {\n                playArc = playArc {\n                    val episode = thaiEp.value\n                    aid = episode.optLong(\"aid\")\n                    cid = episode.optLong(\"cid\")\n                    videoType = BizType.BIZ_TYPE_PGC\n                    episode.optJSONObject(\"dimension\")?.run {\n                        dimension = dimension {\n                            width = optLong(\"width\")\n                            height = optLong(\"height\")\n                            rotate = optLong(\"rotate\")\n                        }\n                    }\n                }\n            }\n        }.toByteArray()\n        response.javaClass.callStaticMethod(\"parseFrom\", newRes)\n    }.onFailure { Log.e(it) }.getOrDefault(response)\n\n    private fun PlayViewReplyKt.Dsl.fixBusinessProto(\n        thaiSeason: Lazy<JSONObject>,\n        thaiEp: Lazy<JSONObject>,\n        jsonContent: JSONObject\n    ) {\n        if (hasBusiness()) {\n            business = business.copy {\n                isPreview = jsonContent.optInt(\"is_preview\", 0) == 1\n                episodeInfo = episodeInfo.copy {\n                    seasonInfo = seasonInfo.copy {\n                        rights = seasonRights {\n                            canWatch = 1\n                        }\n                    }\n                }\n            }\n        } else {\n            // thai\n            business = businessInfo {\n                val season = thaiSeason.value\n                val episode = thaiEp.value\n                isPreview = jsonContent.optInt(\"is_preview\", 0) == 1\n                episodeInfo = episodeInfo {\n                    epId = episode.optInt(\"id\")\n                    cid = episode.optLong(\"id\")\n                    aid = season.optLong(\"season_id\")\n                    epStatus = episode.optLong(\"status\")\n                    cover = episode.optString(\"cover\")\n                    title = episode.optString(\"title\")\n                    seasonInfo = seasonInfo {\n                        seasonId = season.optInt(\"season_id\")\n                        seasonType = season.optInt(\"type\")\n                        seasonStatus = season.optInt(\"status\")\n                        cover = season.optString(\"cover\")\n                        title = season.optString(\"title\")\n                        rights = seasonRights {\n                            canWatch = 1\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private fun JSONObject.toVideoInfo(preferCodec: CodeType, isDownload: Boolean) = videoInfo {\n        val qualityList = optJSONArray(\"accept_quality\")\n            ?.asSequence<Int>()?.toList().orEmpty()\n        val type = optString(\"type\")\n        val videoCodecId = optInt(\"video_codecid\")\n        val formatMap = HashMap<Int, JSONObject>()\n        for (format in optJSONArray(\"support_formats\").orEmpty()) {\n            formatMap[format.optInt(\"quality\")] = format\n        }\n\n        timelength = optLong(\"timelength\")\n        videoCodecid = videoCodecId\n        quality = optInt(\"quality\")\n        format = optString(\"format\")\n\n        if (type == \"DASH\") {\n            val audioIds = ArrayList<Int>()\n            for (audio in optJSONObject(\"dash\")?.optJSONArray(\"audio\").orEmpty()) {\n                dashAudio += dashItem {\n                    audio.run {\n                        baseUrl = optString(\"base_url\")\n                        id = optInt(\"id\")\n                        audioIds.add(id)\n                        md5 = optString(\"md5\")\n                        size = optLong(\"size\")\n                        codecid = optInt(\"codecid\")\n                        bandwidth = optInt(\"bandwidth\")\n                        for (bk in optJSONArray(\"backup_url\").orEmpty().asSequence<String>())\n                            backupUrl += bk\n                    }\n                }\n            }\n            var bestMatchQn = quality\n            var minDeltaQn = Int.MAX_VALUE\n            val preferCodecId = codecMap[preferCodec] ?: videoCodecId\n            val videos = optJSONObject(\"dash\")?.optJSONArray(\"video\")\n                ?.asSequence<JSONObject>()?.toList().orEmpty()\n            val availableQns = videos.map { it.optInt(\"id\") }.toSet()\n            val preferVideos = videos.filter { it.optInt(\"codecid\") == preferCodecId }\n                .takeIf { l -> l.map { it.optInt(\"id\") }.containsAll(availableQns) }\n                ?: videos.filter { it.optInt(\"codecid\") == videoCodecId }\n            preferVideos.forEach { video ->\n                streamList += stream {\n                    dashVideo = dashVideo {\n                        video.run {\n                            baseUrl = optString(\"base_url\")\n                            backupUrl += optJSONArray(\"backup_url\").orEmpty()\n                                .asSequence<String>().toList()\n                            bandwidth = optInt(\"bandwidth\")\n                            codecid = optInt(\"codecid\")\n                            md5 = optString(\"md5\")\n                            size = optLong(\"size\")\n                        }\n                        // Not knowing the extract matching,\n                        // just use the largest id\n                        audioId = audioIds.maxOrNull() ?: audioIds[0]\n                        noRexcode = optInt(\"no_rexcode\") != 0\n                    }\n                    streamInfo = streamInfo {\n                        quality = video.optInt(\"id\")\n                        val deltaQn = abs(quality - this@videoInfo.quality)\n                        if (deltaQn < minDeltaQn) {\n                            bestMatchQn = quality\n                            minDeltaQn = deltaQn\n                        }\n                        intact = true\n                        attribute = 0\n                        formatMap[quality]?.let { fmt ->\n                            reconstructFormat(fmt)\n                        }\n                    }\n                }\n            }\n            quality = bestMatchQn\n        } else if (type == \"FLV\" || type == \"MP4\") {\n            qualityList.forEach { quality ->\n                streamList += stream {\n                    streamInfo = streamInfo {\n                        this.quality = quality\n                        intact = true\n                        attribute = 0\n                        formatMap[quality]?.let { fmt ->\n                            reconstructFormat(fmt)\n                        }\n                    }\n\n                    if (quality == optInt(\"quality\")) {\n                        segmentVideo = segmentVideo {\n                            for (seg in optJSONArray(\"durl\").orEmpty()) {\n                                segment += responseUrl {\n                                    seg.run {\n                                        length = optLong(\"length\")\n                                        backupUrl += optJSONArray(\"backup_url\").orEmpty()\n                                            .asSequence<String>().toList()\n                                        md5 = optString(\"md5\")\n                                        order = optInt(\"order\")\n                                        size = optLong(\"size\")\n                                        url = optString(\"url\")\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        reconstructVideoInfoUpos(isDownload)\n        if (isDownload) {\n            fixDownloadProto(true)\n        }\n    }\n\n    private fun StreamInfoKt.Dsl.reconstructFormat(fmt: JSONObject) = fmt.run {\n        description = optString(\"description\")\n        format = optString(\"format\")\n        needVip = optBoolean(\"need_vip\", false)\n        needLogin = optBoolean(\"need_login\", false)\n        newDescription = optString(\"new_description\")\n        superscript = optString(\"superscript\")\n        displayDesc = optString(\"display_desc\")\n    }\n\n    private fun purifyViewInfo(response: Any, supplement: PlayViewReply? = null) = runCatching {\n        supplement?.copy {\n            playExtConf = playExtConf.copy { clearFreyaConfig() }\n            viewInfo = viewInfo.copy {\n                clearAnimation()\n                clearCouponInfo()\n                if (endPage.dialog.type != \"pay\")\n                    clearEndPage()\n                clearHighDefinitionTrialInfo()\n                clearPayTip()\n                if (popWin.buttonList.all { it.actionType != \"pay\" })\n                    clearPopWin()\n                if (toast.button.actionType != \"pay\")\n                    clearToast()\n                if (tryWatchPromptBar.buttonList.all { it.actionType != \"pay\" })\n                    clearTryWatchPromptBar()\n                extToast.clear()\n            }\n        }?.let {\n            val serializedResponse = response.callMethodAs<ByteArray>(\"toByteArray\")\n            val newRes = PlayViewUniteReply.parseFrom(serializedResponse).copy {\n                this.supplement = any {\n                    typeUrl = PGC_ANY_MODEL_TYPE_URL\n                    value = it.toByteString()\n                }\n            }.toByteArray()\n            response.javaClass.callStaticMethod(\"parseFrom\", newRes)\n        } ?: run {\n            response.callMethodOrNull(\"getPlayExtConf\")\n                ?.callMethod(\"clearFreyaConfig\")\n            response.callMethod(\"getViewInfo\")?.run {\n                callMethodOrNull(\"clearAnimation\")\n                callMethod(\"clearCouponInfo\")\n                if (callMethod(\"getEndPage\")\n                        ?.callMethod(\"getDialog\")\n                        ?.callMethod(\"getType\") != \"pay\"\n                ) callMethod(\"clearEndPage\")\n                callMethod(\"clearHighDefinitionTrialInfo\")\n                callMethod(\"clearPayTip\")\n                if (callMethod(\"getPopWin\")\n                        ?.callMethodAs<List<Any>>(\"getButtonList\")\n                        ?.all { it.callMethod(\"getActionType\") != \"pay\" } != false\n                ) callMethod(\"clearPopWin\")\n                if (callMethod(\"getToast\")\n                        ?.callMethod(\"getButton\")\n                        ?.callMethod(\"getActionType\") != \"pay\"\n                ) callMethod(\"clearToast\")\n                if (callMethod(\"getTryWatchPromptBar\")\n                        ?.callMethodAs<List<Any>>(\"getButtonList\")\n                        ?.all { it.callMethod(\"getActionType\") != \"pay\" } != false\n                ) callMethod(\"clearTryWatchPromptBar\")\n                callMethodOrNullAs<LinkedHashMap<*, *>>(\"internalGetMutableExtToast\")?.clear()\n            }\n            response\n        }\n    }.onFailure { Log.e(it) }.getOrDefault(response)\n\n    private fun VideoInfoKt.Dsl.reconstructVideoInfoUpos(isDownload: Boolean = false) {\n        if (!isDownload || !enableUposReplace) return\n        val newStreamList = streamList.map { stream ->\n            stream.copy { reconstructStreamUpos() }\n        }\n        val newDashAudio = dashAudio.map { dashItem ->\n            dashItem.copy { reconstructDashItemUpos() }\n        }\n        streamList.clear()\n        dashAudio.clear()\n        streamList.addAll(newStreamList)\n        dashAudio.addAll(newDashAudio)\n    }\n\n    private fun StreamKt.Dsl.reconstructStreamUpos() {\n        if (hasDashVideo()) {\n            dashVideo = dashVideo.copy {\n                if (!hasBaseUrl()) return@copy\n                val (newBaseUrl, newBackupUrl) = reconstructVideoInfoUpos(baseUrl, backupUrl)\n                baseUrl = newBaseUrl\n                backupUrl.clear()\n                backupUrl.addAll(newBackupUrl)\n            }\n        } else if (hasSegmentVideo()) {\n            segmentVideo = segmentVideo.copy {\n                val newSegment = segment.map { responseUrl ->\n                    responseUrl.copy {\n                        val (newUrl, newBackupUrl) = reconstructVideoInfoUpos(url, backupUrl)\n                        url = newUrl\n                        backupUrl.clear()\n                        backupUrl.addAll(newBackupUrl)\n                    }\n                }\n                segment.clear()\n                segment.addAll(newSegment)\n            }\n        }\n    }\n\n    private fun DashItemKt.Dsl.reconstructDashItemUpos() {\n        if (!hasBaseUrl()) return\n        val (newBaseUrl, newBackupUrl) = reconstructVideoInfoUpos(baseUrl, backupUrl)\n        baseUrl = newBaseUrl\n        backupUrl.clear()\n        backupUrl.addAll(newBackupUrl)\n    }\n\n    private fun reconstructVideoInfoUpos(\n        baseUrl: String, backupUrls: List<String>\n    ): Pair<String, List<String>> {\n        val filteredBackupUrls = backupUrls.filter { !it.isPCdnUpos() }\n        val rawUrl = filteredBackupUrls.firstOrNull() ?: baseUrl\n        return if (baseUrl.isPCdnUpos()) {\n            if (filteredBackupUrls.isNotEmpty()) {\n                rawUrl.replaceUpos() to listOf(rawUrl.replaceUpos(videoUposBackups[0]), baseUrl)\n            } else baseUrl to backupUrls\n        } else {\n            baseUrl.replaceUpos() to listOf(\n                rawUrl.replaceUpos(videoUposBackups[0]),\n                rawUrl.replaceUpos(videoUposBackups[1])\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/BangumiSeasonHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.content.res.Configuration\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport com.google.protobuf.any\nimport kotlinx.coroutines.*\nimport me.iacn.biliroaming.*\nimport me.iacn.biliroaming.API.*\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.Constant.TYPE_EPISODE_ID\nimport me.iacn.biliroaming.Constant.TYPE_SEASON_ID\nimport me.iacn.biliroaming.network.BiliRoamingApi\nimport me.iacn.biliroaming.network.BiliRoamingApi.getAreaSearchBangumi\nimport me.iacn.biliroaming.network.BiliRoamingApi.getContent\nimport me.iacn.biliroaming.network.BiliRoamingApi.getSeason\nimport me.iacn.biliroaming.network.BiliRoamingApi.getSpace\nimport me.iacn.biliroaming.utils.*\nimport org.json.JSONObject\nimport java.io.InputStream\nimport java.lang.reflect.Method\nimport java.net.URL\nimport java.util.*\nimport java.util.concurrent.TimeUnit\nimport kotlin.Array\nimport kotlin.Boolean\nimport kotlin.Int\nimport java.lang.reflect.Array as RArray\n\n/**\n * Created by iAcn on 2019/3/27\n * Email i@iacn.me\n */\nclass BangumiSeasonHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    companion object {\n        val lastSeasonInfo: MutableMap<String, String?> = HashMap()\n\n        private val jsonNonStrict = lazy {\n            instance.kotlinJsonClass?.getStaticObjectField(\"Companion\")?.callMethod(\"getNonstrict\")\n        }\n        const val FAIL_CODE = -404\n\n        data class Area(val area: String, val text: String, val type: String, val typeStr: String)\n\n        private val AREA_TYPES = mapOf(\n            931 to Area(\"cn\", \"陆(影)\", \"8\", \"movie\"),\n            364364 to Area(\"hk\", \"港(影)\", \"8\", \"movie\"),\n            889464 to Area(\"tw\", \"台(影)\", \"8\", \"movie\"),\n            114 to Area(\"th\", \"泰\", \"7\", \"bangumi\"),\n            514 to Area(\"cn\", \"陆\", \"7\", \"bangumi\"),\n            1919 to Area(\"hk\", \"港\", \"7\", \"bangumi\"),\n            810 to Area(\"tw\", \"台\", \"7\", \"bangumi\")\n        )\n\n        private const val PGC_ANY_MODEL_TYPE_URL =\n            \"type.googleapis.com/bilibili.app.viewunite.pgcanymodel.ViewPgcAny\"\n        private const val UGC_ANY_MODEL_TYPE_URL =\n            \"type.googleapis.com/bilibili.app.viewunite.ugcanymodel.ViewUgcAny\"\n\n        private val needUnlockDownload = sPrefs.getBoolean(\"allow_download\", false)\n    }\n\n    private val isSerializable by lazy {\n        \"com.bilibili.bangumi.data.page.detail.entity.BangumiUniformSeason\\$\\$serializer\".findClassOrNull(\n            mClassLoader\n        ) != null\n    }\n\n    private val isGson by lazy {\n        instance.bangumiUniformSeasonClass?.annotations?.any {\n            it.annotationClass.java.name.startsWith(\"gsonannotator\")\n        } ?: false && instance.gsonFromJson() != null && instance.gsonToJson() != null\n    }\n\n    private val gson by lazy {\n        instance.gson()?.let { instance.gsonConverterClass?.getStaticObjectField(it) }\n    }\n\n    private val serializerFeatures = lazy {\n        val serializerFeatureClass =\n            \"com.alibaba.fastjson.serializer.SerializerFeature\".findClassOrNull(mClassLoader)\n                ?: return@lazy null\n        val keyAsString = serializerFeatureClass.getStaticObjectField(\"WriteNonStringKeyAsString\")\n        val noDefault = serializerFeatureClass.getStaticObjectField(\"NotWriteDefaultValue\")\n        val serializerFeatures = RArray.newInstance(serializerFeatureClass, 2)\n        RArray.set(serializerFeatures, 0, keyAsString)\n        RArray.set(serializerFeatures, 1, noDefault)\n        serializerFeatures\n    }\n\n    private fun Any.toJson() = when {\n        isSerializable -> jsonNonStrict.value?.callMethodAs<String>(\n            \"stringify\",\n            javaClass.getStaticObjectField(\"Companion\")?.callMethod(\"serializer\"),\n            this\n        ).toJSONObject()\n\n        isGson -> gson?.callMethodAs<String>(instance.gsonToJson(), this)?.toJSONObject()\n        else -> instance.fastJsonClass?.callStaticMethodAs<String>(\n            \"toJSONString\",\n            this,\n            serializerFeatures.value\n        ).toJSONObject()\n    }\n\n    private fun Class<*>.fromJson(json: String) = when {\n        isSerializable -> jsonNonStrict.value?.callMethod(\n            \"parse\",\n            getStaticObjectField(\"Companion\")?.callMethod(\"serializer\"),\n            json\n        )\n\n        isGson -> gson?.callMethod(instance.gsonFromJson(), json, this)\n        else -> instance.fastJsonClass?.callStaticMethod(instance.fastJsonParse(), json, this)\n    }\n\n    private fun Class<*>.fromJson(json: JSONObject) = fromJson(json.toString())\n\n    private fun updateSeasonInfo(args: Array<*>, paramMap: Map<String, String>) {\n        lastSeasonInfo.clear()\n        val seasonMode = when {\n            args[1] is Int -> args[1] as Int\n            args[3] != \"0\" && paramMap.containsKey(\"ep_id\") -> TYPE_EPISODE_ID\n            args[2] != \"0\" && paramMap.containsKey(\"season_id\") -> TYPE_SEASON_ID\n            else -> -1\n        }\n        when (seasonMode) {\n            TYPE_SEASON_ID -> lastSeasonInfo[\"season_id\"] = paramMap[\"season_id\"]\n            TYPE_EPISODE_ID -> lastSeasonInfo[\"ep_id\"] = paramMap[\"ep_id\"]\n            else -> return\n        }\n    }\n\n    private val searchAllResultClass by Weak {\n        \"com.bilibili.search.api.SearchResultAll\".findClassOrNull(\n            mClassLoader\n        )\n    }\n    private val searchAllResultNavInfoClass by Weak {\n        \"com.bilibili.search.api.SearchResultAll\\$NavInfo\".findClassOrNull(\n            mClassLoader\n        )\n    }\n    private val bangumiSearchPageClass by Weak {\n        \"com.bilibili.bangumi.data.page.search.BangumiSearchPage\".findClassOrNull(\n            mClassLoader\n        ) ?: \"com.bilibili.search.result.bangumi.ogv.BangumiSearchPage\".findClassOrNull(\n            mClassLoader\n        )\n    }\n    private val biliSearchOgvResultClass by Weak {\n        \"com.bilibili.search.ogv.BiliSearchOgvResult\".findClassOrNull(\n            mClassLoader\n        )\n    }\n    private val baseSearchItemClass by Weak {\n        \"com.bilibili.search.api.BaseSearchItem\".findClassOrNull(\n            mClassLoader\n        )\n    }\n\n    private val navClass by Weak { \"com.bapis.bilibili.polymer.app.search.v1.Nav\" from mClassLoader }\n    private val searchByTypeRespClass by Weak { \"com.bapis.bilibili.polymer.app.search.v1.SearchByTypeResponse\" from mClassLoader }\n\n    @SuppressLint(\"SetTextI18n\")\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"main_func\", false)) return\n        Log.d(\"startHook: BangumiSeason\")\n        instance.seasonParamsMapClass?.hookAfterAllConstructors { param ->\n            @Suppress(\"UNCHECKED_CAST\")\n            val paramMap = param.thisObject as Map<String, String>\n            updateSeasonInfo(param.args, paramMap)\n        }\n\n        instance.seasonParamsClass?.hookAfterAllConstructors { param ->\n            val paramMap =\n                param.thisObject.callMethodAs<Map<String, String>>(instance.paramsToMap())\n            updateSeasonInfo(param.args, paramMap)\n        }\n\n        val invalidTipsHook: Hooker = { param ->\n            val view = param.args[0]?.let {\n                if (it is View) it else it.callMethod(\"getView\") as View?\n            }\n            if (lastSeasonInfo[\"allow_comment\"] == \"0\" &&\n                !sPrefs.getBoolean(\"force_th_comment\", false)\n            ) {\n                view?.findViewById<TextView>(getId(\"info\"))?.text =\n                    \"由于泰区番剧评论会串到其他正常视频中，\\n因而禁用泰区评论，还望理解。\"\n                view?.findViewById<ImageView>(getId(\"forbid_icon\"))?.run {\n                    MainScope().launch {\n                        withContext(Dispatchers.IO) {\n                            URL(\"https://i0.hdslb.com/bfs/album/08d5ce2fef8da8adf91024db4a69919b8d02fd5c.png\")\n                                .openStream().let { BitmapFactory.decodeStream(it) }\n                        }.let { setImageBitmap(it) }\n                    }\n                }\n            }\n        }\n        instance.commentInvalidFragmentClass?.run {\n            hookAfterMethod(\n                \"onViewCreated\",\n                View::class.java,\n                Bundle::class.java,\n                hooker = invalidTipsHook\n            )\n            instance.setInvalidTips()?.let {\n                hookAfterMethod(it, this, \"kotlin.Pair\", hooker = invalidTipsHook)\n            }\n        }\n\n        \"com.bilibili.bangumi.ui.page.detail.BangumiDetailActivityV3\".hookAfterMethod(\n            mClassLoader, \"onCreate\", Bundle::class.java\n        ) {\n            setErrorMessage(it.thisObject as Activity)\n        }\n        \"com.bilibili.bangumi.ui.page.detail.BangumiDetailActivityV3\".hookAfterMethod(\n            mClassLoader, \"onConfigurationChanged\", Configuration::class.java\n        ) {\n            setErrorMessage(it.thisObject as Activity)\n        }\n\n        if (isBuiltIn && is64 && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n            Log.e(\"Not support\")\n            Log.toast(\"Android O以下系统不支持64位Xpatch版，请使用32位版\")\n        } else {\n            instance.retrofitResponseClass?.hookBeforeAllConstructors { param ->\n                val url = getRetrofitUrl(param.args[0])\n                val body = param.args[1] ?: return@hookBeforeAllConstructors\n                if (url?.startsWith(\"https://api.bilibili.com/pgc/view/v2/app/season\") == true &&\n                    body.javaClass == instance.okioWrapperClass\n                ) {\n                    val okioBuffer = body.getObjectField(instance.okio())\n                    val json =\n                        okioBuffer?.callMethodAs<InputStream?>(instance.okioInputStream())?.use {\n                            it.readBytes().toString(Charsets.UTF_8).toJSONObject()\n                        }?.apply {\n                            put(\n                                \"data\",\n                                fixBangumi(optJSONObject(\"data\"), optInt(\"code\", FAIL_CODE), url)\n                            )\n                            put(\"code\", 0)\n                        }\n                    val newStream = json?.toString()?.byteInputStream()\n                    body.setObjectField(\n                        instance.okio(),\n                        okioBuffer?.javaClass?.new()?.apply {\n                            callMethod(instance.okioReadFrom(), newStream)\n                        })\n                    body.setLongField(instance.okioLength(), newStream?.available()?.toLong() ?: 0L)\n                }\n                // Filter non-bangumi responses\n                // If it isn't bangumi, the type variable will not exist in this map\n                if (instance.bangumiApiResponseClass?.isInstance(body) == true ||\n                    // for new blue 6.3.7\n                    instance.rxGeneralResponseClass?.isInstance(body) == true\n                ) {\n                    fixBangumi(body, url)\n                }\n                if (url != null && url.startsWith(\"https://appintl.biliapi.net/intl/gateway/app/search/type\")\n                ) {\n                    val area = Uri.parse(url).getQueryParameter(\"type\")?.toInt()\n                    if (!AREA_TYPES.containsKey(area)) {\n                        fixPlaySearchType(body, url)\n                    }\n                }\n                if (instance.generalResponseClass?.isInstance(body) == true ||\n                    instance.rxGeneralResponseClass?.isInstance(body) == true\n                ) {\n                    val dataField =\n                        if (instance.generalResponseClass?.isInstance(body) == true) \"data\" else instance.responseDataField()\n                    val data = body.getObjectField(dataField)\n                    if (data?.javaClass == searchAllResultClass) {\n                        addAreaTags(data)\n                    }\n                    url ?: return@hookBeforeAllConstructors\n                    if (url.startsWith(\"https://app.bilibili.com/x/v2/search/type\") || url.startsWith(\n                            \"https://appintl.biliapi.net/intl/gateway/app/search/type\"\n                        ) || url.startsWith(\"https://app.bilibili.com/x/intl/search/type\")\n                    ) {\n                        val area = Uri.parse(url).getQueryParameter(\"type\")?.toInt()\n                        if (AREA_TYPES.containsKey(area)) {\n                            if (data?.javaClass == bangumiSearchPageClass) {\n                                body.setObjectField(dataField, AREA_TYPES[area]?.let {\n                                    retrieveAreaSearch(data, url, it.area, it.type)\n                                })\n                            } else if (data?.javaClass == biliSearchOgvResultClass) {\n                                body.setObjectField(dataField, AREA_TYPES[area]?.let {\n                                    retrieveAreaSearchV2(data, url, it.area, it.type)\n                                })\n                            }\n                        }\n                    } else if (url.startsWith(\"https://app.bilibili.com/x/v2/view?\") ||\n                        url.startsWith(\"https://app.bilibili.com/x/intl/view?\") ||\n                        url.startsWith(\"https://appintl.biliapi.net/intl/gateway/app/view?\") &&\n                        body.getIntField(\"code\") == FAIL_CODE\n                    ) {\n                        fixView(data, url)?.let {\n                            body.setObjectField(dataField, it)\n                            body.setIntField(\"code\", 0)\n                        }\n                    } else if (url.startsWith(\"https://app.bilibili.com/x/v2/space?\")) {\n                        val code = body.getIntFieldOrNull(\"code\")\n                        val mid = Uri.parse(url).getQueryParameter(\"vmid\")?.toLongOrNull()\n                        if (code != 0 && sPrefs.getBoolean(\"fix_space\", false)) {\n                            fixSpace(mid)?.let {\n                                body.setObjectField(dataField, it)\n                                body.setIntField(\"code\", 0)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        instance.viewMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeView\" else \"view\",\n            instance.viewReqClass\n        ) { param ->\n            param.result?.let { return@hookAfterMethod }\n            val serializedRequest = param.args[0].callMethodAs<ByteArray>(\"toByteArray\")\n            val req = ViewReq.parseFrom(serializedRequest)\n            val reply = fixViewProto(req)\n            val serializedReply = reply?.toByteArray() ?: return@hookAfterMethod\n            param.result =\n                (param.method as Method).returnType.callStaticMethod(\"parseFrom\", serializedReply)\n        }\n\n        instance.viewUniteMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeView\" else \"view\",\n            \"com.bapis.bilibili.app.viewunite.v1.ViewReq\"\n        ) { param ->\n            if (instance.networkExceptionClass?.isInstance(param.throwable) == true) return@hookAfterMethod\n            val response = param.result\n            if (response == null) {\n                val req = param.args[0].callMethodAs<ByteArray>(\"toByteArray\").let {\n                    ViewUniteReq.parseFrom(it)\n                }\n                val av = (if (req.hasAid()) req.aid.takeIf { it != 0L } else if (req.hasBvid()) bv2av(req.bvid)  else null)?.toString()\n                fixViewProto(req, av)?.toByteArray()?.let {\n                    param.result =\n                            \"com.bapis.bilibili.app.viewunite.v1.ViewReply\".from(mClassLoader)\n                                    ?.callStaticMethod(\"parseFrom\", it)\n                } ?: Log.toast(\"解锁失败！\", force = true)\n                return@hookAfterMethod\n            }\n            val supplementAny = response.callMethod(\"getSupplement\")\n            val typeUrl = supplementAny?.callMethodAs<String>(\"getTypeUrl\")\n            // Only handle pgc video\n            if (param.result != null && typeUrl != PGC_ANY_MODEL_TYPE_URL) {\n                return@hookAfterMethod\n            }\n            val supplement =\n                supplementAny?.callMethod(\"getValue\")?.callMethodAs<ByteArray>(\"toByteArray\")\n                    ?.let { ViewPgcAny.parseFrom(it) } ?: viewPgcAny {}\n\n            fixViewProto(response, supplement)\n        }\n\n        val urlHook: Hooker = fun(param) {\n            val redirectUrl = param.thisObject.getObjectFieldAs<String?>(\"redirectUrl\")\n            if (redirectUrl.isNullOrEmpty()) return\n            param.result = param.thisObject.callMethod(\"getUrl\", redirectUrl)\n        }\n        \"com.bilibili.bplus.followingcard.api.entity.cardBean.VideoCard\".from(mClassLoader)?.run {\n            hookAfterMethod(\"getJumpUrl\", hooker = urlHook)\n            hookAfterMethod(\"getCommentJumpUrl\", hooker = urlHook)\n        }\n\n        if (sPrefs.getBoolean(\"hidden\", false) &&\n            (sPrefs.getBoolean(\"search_area_bangumi\", false)\n                    || sPrefs.getBoolean(\"search_area_movie\", false))\n        ) {\n            val searchResultFragment =\n                \"com.bilibili.bangumi.ui.page.search.BangumiSearchResultFragment\".from(mClassLoader)\n                    ?: \"com.bilibili.search.result.bangumi.ogv.BangumiSearchResultFragment\"\n                        .from(mClassLoader) ?: \"com.bilibili.search.ogv.OgvSearchResultFragment\"\n                        .from(mClassLoader) ?: \"com.bilibili.search2.ogv.OgvSearchResultFragment\"\n                        .from(mClassLoader)\n            searchResultFragment?.run {\n                val intTypeFields = declaredFields.filter {\n                    it.type == Int::class.javaPrimitiveType\n                }.onEach { it.isAccessible = true }\n                hookBeforeMethod(\n                    \"setUserVisibleCompat\",\n                    Boolean::class.javaPrimitiveType\n                ) { param ->\n                    param.thisObject.callMethodAs<Bundle>(\"getArguments\").run {\n                        val from = getString(\"from\")\n                        for (area in AREA_TYPES) {\n                            if (from == area.value.area) {\n                                intTypeFields.forEach {\n                                    if (it.get(param.thisObject) == area.value.type.toInt())\n                                        it.set(param.thisObject, area.key)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        if (sPrefs.getBoolean(\"hidden\", false) &&\n            (sPrefs.getBoolean(\"search_area_bangumi\", false)\n                    || sPrefs.getBoolean(\"search_area_movie\", false))\n        ) {\n            val mossResponseHandlerClass = instance.mossResponseHandlerClass ?: return\n            val searchMossClass =\n                \"com.bapis.bilibili.polymer.app.search.v1.SearchMoss\".from(mClassLoader) ?: return\n            searchMossClass.hookBeforeMethod(\n                \"searchAll\",\n                \"com.bapis.bilibili.polymer.app.search.v1.SearchAllRequest\",\n                mossResponseHandlerClass\n            ) { param ->\n                param.args[1] = param.args[1].mossResponseHandlerProxy {\n                    addAreaTagsV2(it)\n                }\n            }\n            searchMossClass.hookBeforeMethod(\n                \"searchByType\",\n                \"com.bapis.bilibili.polymer.app.search.v1.SearchByTypeRequest\",\n                mossResponseHandlerClass\n            ) { param ->\n                val searchByTypeRespClass = searchByTypeRespClass ?: return@hookBeforeMethod\n                val key =\n                    param.args[0].callMethodOrNullAs<Int>(\"getType\") ?: return@hookBeforeMethod\n                val areaType = AREA_TYPES[key] ?: return@hookBeforeMethod\n                val request = SearchByTypeRequest.parseFrom(\n                    param.args[0].callMethodAs<ByteArray>(\"toByteArray\")\n                )\n                val type = areaType.type\n                val area = areaType.area\n                val handler = param.args[1]\n                MainScope().launch(Dispatchers.IO) {\n                    val result = retrieveAreaSearchV3(request, area, type)\n                    if (result != null) {\n                        val newRes = searchByTypeRespClass\n                            .callStaticMethod(\"parseFrom\", result.toByteArray())\n                        handler.callMethod(\"onNext\", newRes)\n                        handler.callMethod(\"onCompleted\")\n                    } else {\n                        handler.callMethod(\"onError\", null)\n                    }\n                }\n                param.result = null\n            }\n        }\n    }\n\n    private fun addAreaTagsV2(v: Any?) {\n        v ?: return\n        val navClass = navClass ?: return\n        val navList = v.callMethodAs<List<Any>>(\"getNavList\")\n            .map { SearchNav.parseFrom(it.callMethodAs<ByteArray>(\"toByteArray\")) }\n            .toMutableList()\n        // fix crash when searching sensitive words\n        if (navList.size == 0) {\n            return\n        }\n        val currentArea = runCatchingOrNull {\n            XposedInit.country.get(5L, TimeUnit.SECONDS)\n        }\n        for (area in AREA_TYPES) {\n            if (area.value.area == currentArea)\n                continue\n            if (!sPrefs.getString(area.value.area + \"_server\", null).isNullOrBlank() &&\n                sPrefs.getBoolean(\"search_area_\" + area.value.typeStr, false)\n            ) {\n                val nav = searchNav {\n                    name = area.value.text\n                    total = 0\n                    pages = 0\n                    type = area.key\n                }\n                navList.add(1, nav)\n            }\n        }\n        v.callMethod(\"clearNav\")\n        val newNavList = navList.map {\n            navClass.callStaticMethod(\"parseFrom\", it.toByteArray())\n        }\n        v.callMethod(\"addAllNav\", newNavList)\n    }\n\n    private fun Class<*>.reconstructPageType() {\n        val pageArray = getStaticObjectFieldAs<Array<Any>>(\"\\$VALUES\")\n        val extra = AREA_TYPES.mapNotNull { area ->\n            sPrefs.getString(area.value.area + \"_server\", null)\n                .takeUnless { it.isNullOrBlank() }?.run {\n                    new(\n                        \"PAGE_\" + area.value.typeStr.uppercase(Locale.getDefault()), 4,\n                        \"bilibili://search-result/new-\" + area.value.typeStr + \"?from=\" + area.value.area,\n                        area.key, area.value.typeStr\n                    )\n                }\n        }\n        val newPageArray = pageArray.copyOf(pageArray.size + extra.size)\n        extra.forEachIndexed { index, any -> newPageArray[pageArray.size + index] = any }\n        setStaticObjectField(\"\\$VALUES\", newPageArray)\n    }\n\n    override fun lateInitHook() {\n        if (sPrefs.getBoolean(\"hidden\", false) &&\n            (sPrefs.getBoolean(\"search_area_bangumi\", false)\n                    || sPrefs.getBoolean(\"search_area_movie\", false))\n        ) {\n            \"com.bilibili.search.result.pages.BiliMainSearchResultPage\\$PageTypes\".findClassOrNull(mClassLoader)?.reconstructPageType()\n            \"com.bilibili.search2.result.pages.BiliMainSearchResultPage\\$PageTypes\".findClassOrNull(mClassLoader)?.reconstructPageType()\n        }\n    }\n\n    private fun retrieveAreaSearchV3(\n        request: SearchByTypeRequest,\n        area: String,\n        type: String\n    ): SearchByTypeResponse? {\n        val pn = request.pagination.next.ifEmpty { \"1\" }\n        val ps = request.pagination.pageSize\n        val keyword = request.keyword\n        val query = mapOf(\n            \"access_key\" to (instance.accessKey ?: \"\"),\n            \"fnval\" to request.playerArgs.fnval.toString(),\n            \"fnver\" to request.playerArgs.fnver.toString(),\n            \"qn\" to request.playerArgs.qn.toString(),\n            \"pn\" to pn,\n            \"ps\" to ps.toString(),\n            \"keyword\" to keyword,\n        )\n        val jsonContent = getAreaSearchBangumi(query, area, type)?.toJSONObject()\n            ?: return null\n        checkErrorToast(jsonContent, true)\n        val newData = jsonContent.optJSONObject(\"data\") ?: return null\n\n        fun ReasonStyleKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            text = optString(\"text\")\n            textColor = optString(\"text_color\")\n            textColorNight = optString(\"text_color_night\")\n            bgColor = optString(\"bg_color\")\n            bgColorNight = optString(\"bg_color_night\")\n            borderColor = optString(\"border_color\")\n            borderColorNight = optString(\"border_color_night\")\n            bgStyle = optInt(\"bg_style\")\n        }\n\n        fun EpisodeKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            uri = optString(\"uri\")\n            param = optString(\"param\")\n            index = optString(\"index\")\n            for (badge in optJSONArray(\"badges\").orEmpty())\n                badges += reasonStyle { reconstructFrom(badge) }\n            position = optInt(\"position\")\n        }\n\n        fun EpisodeNewKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            title = optString(\"title\")\n            uri = optString(\"uri\")\n            param = optString(\"param\")\n            isNew = optInt(\"is_new\")\n            for (badge in optJSONArray(\"badges\").orEmpty())\n                badges += reasonStyle { reconstructFrom(badge) }\n            this@reconstructFrom.type = optInt(\"type\")\n            position = optInt(\"position\")\n            cover = optString(\"cover\")\n            label = optString(\"label\")\n        }\n\n        fun WatchButtonKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            title = optString(\"title\")\n            link = optString(\"link\")\n        }\n\n        fun CheckMoreKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            content = optString(\"content\")\n            uri = optString(\"uri\")\n        }\n\n        fun FollowButtonKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            icon = optString(\"icon\")\n            optJSONObject(\"texts\")?.let { o ->\n                o.keys().asSequence().associateWith { o.opt(it)?.toString() ?: \"\" }\n            }?.let { texts.putAll(it) }\n            statusReport = optString(\"status_report\")\n        }\n\n        fun SearchBangumiCardKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            title = optString(\"title\")\n            cover = optString(\"cover\")\n            mediaType = optInt(\"media_type\")\n            playState = optInt(\"play_state\")\n            this@reconstructFrom.area = optString(\"area\")\n            style = optString(\"style\")\n            styles = optString(\"styles\")\n            cv = optString(\"cv\")\n            rating = optDouble(\"rating\")\n            vote = optInt(\"vote\")\n            target = optString(\"target\")\n            staff = optString(\"staff\")\n            prompt = optString(\"prompt\")\n            ptime = optLong(\"ptime\")\n            seasonTypeName = optString(\"season_type_name\")\n            for (episode in optJSONArray(\"episodes\").orEmpty())\n                episodes += episode { reconstructFrom(episode) }\n            isSelection = optInt(\"is_selection\")\n            isAtten = optInt(\"is_atten\")\n            label = optString(\"label\")\n            seasonId = optLong(\"season_id\")\n            outName = optString(\"out_name\")\n            outIcon = optString(\"out_icon\")\n            outUrl = optString(\"out_url\")\n            for (badge in optJSONArray(\"badges\").orEmpty())\n                badges += reasonStyle { reconstructFrom(badge) }\n            isOut = optInt(\"is_out\")\n            for (episodeNew in optJSONArray(\"episodes_new\").orEmpty())\n                episodesNew += episodeNew { reconstructFrom(episodeNew) }\n            optJSONObject(\"watch_button\")?.let {\n                watchButton = watchButton { reconstructFrom(it) }\n            }\n            selectionStyle = optString(\"selection_style\")\n            optJSONObject(\"check_more\")?.let {\n                checkMore = checkMore { reconstructFrom(it) }\n            }\n            optJSONObject(\"follow_button\")?.let {\n                followButton = followButton { reconstructFrom(it) }\n            }\n            optJSONObject(\"style_label\")?.let {\n                styleLabel = reasonStyle { reconstructFrom(it) }\n            }\n            for (badgeV2 in optJSONArray(\"badges_v2\").orEmpty())\n                badgesV2 += reasonStyle { reconstructFrom(badgeV2) }\n            stylesV2 = optString(\"styles_v2\")\n        }\n\n        fun SearchItemKt.Dsl.reconstructFrom(json: JSONObject) = json.run {\n            uri = optString(\"uri\")\n            param = optString(\"param\")\n            goto = optString(\"goto\")\n            linkType = optString(\"link_type\")\n            position = optInt(\"position\")\n            trackId = optString(\"track_id\")\n            bangumi = searchBangumiCard { reconstructFrom(json) }\n        }\n\n        val pages = newData.optInt(\"pages\")\n        var page = pn.toIntOrNull() ?: 1\n        val response = searchByTypeResponse {\n            this.pages = pages\n            this.keyword = keyword\n            for (json in newData.optJSONArray(\"items\").orEmpty()) {\n                if (json.optInt(\"Offset\", -1) != -1)\n                    json.remove(\"follow_button\")\n                items += searchItem { reconstructFrom(json) }\n            }\n            if (page < pages)\n                pagination = paginationReply { next = (++page).toString() }\n        }\n        return response\n    }\n\n    private fun fixSpace(mid: Long?): Any? {\n        mid ?: return null\n        instance.biliSpaceClass ?: return null\n        return getSpace(mid)?.let {\n            instance.fastJsonClass?.callStaticMethodOrNull(\n                instance.fastJsonParse(),\n                it,\n                instance.biliSpaceClass\n            )\n        }\n    }\n\n    private fun retrieveAreaSearchV2(data: Any?, url: String, area: String, type: String): Any? {\n        data ?: return null\n        if (sPrefs.getBoolean(\"hidden\", false) && (sPrefs.getBoolean(\n                \"search_area_bangumi\", false\n            ) || sPrefs.getBoolean(\"search_area_movie\", false))\n        ) {\n            val query = Uri.parse(url).run {\n                queryParameterNames.associateWith { getQueryParameter(it) ?: \"\" }\n            }\n            val content = getAreaSearchBangumi(query, area, type) ?: return data\n            val jsonContent = content.toJSONObject()\n            checkErrorToast(jsonContent, true)\n            val newData = jsonContent.optJSONObject(\"data\") ?: return data\n\n            val newItems = mutableListOf<Any>()\n            for (item in newData.optJSONArray(\"items\").orEmpty()) {\n                // 去除追番按钮\n                if (item.optInt(\"Offset\", -1) != -1) {\n                    item.remove(\"follow_button\")\n                }\n                val goto = baseSearchItemClass?.getStaticObjectFieldAs<Map<String, Any>>(\"sMap\")\n                    ?.get(item.optString(\"goto\"))\n                instance.fastJsonClass?.callStaticMethod(\n                    instance.fastJsonParse(), item.toString(), goto?.callMethod(\"getImpl\")\n                )?.let {\n                    it.setIntField(\"viewType\", goto?.callMethodAs<String>(\"getLayout\").hashCode())\n                    newItems.add(it)\n                }\n            }\n            return data.javaClass.new()\n                .setObjectField(\"items\", newItems)\n                .setIntField(\"totalPages\", newData.optInt(\"pages\"))\n        } else {\n            return data\n        }\n    }\n\n    private fun retrieveAreaSearch(data: Any?, url: String, area: String, type: String): Any? {\n        data ?: return null\n        if (sPrefs.getBoolean(\"hidden\", false) && (sPrefs.getBoolean(\n                \"search_area_bangumi\", false\n            ) || sPrefs.getBoolean(\"search_area_movie\", false))\n        ) {\n            val query = Uri.parse(url).run {\n                queryParameterNames.associateWith { getQueryParameter(it) ?: \"\" }\n            }\n            val content = getAreaSearchBangumi(query, area, type) ?: return data\n            val jsonContent = content.toJSONObject()\n            checkErrorToast(jsonContent, true)\n            val newData = jsonContent.optJSONObject(\"data\") ?: return data\n\n            // 去除追番按钮\n            for (item in newData.optJSONArray(\"items\").orEmpty()) {\n                if (item.optInt(\"Offset\", -1) != -1) {\n                    item.remove(\"follow_button\")\n                }\n            }\n\n            return instance.fastJsonClass?.callStaticMethod(\n                instance.fastJsonParse(),\n                newData.toString(),\n                data.javaClass\n            ) ?: data\n        } else {\n            return data\n        }\n    }\n\n    private fun addAreaTags(body: Any?) {\n        body ?: return\n        if (sPrefs.getBoolean(\"hidden\", false) &&\n            (sPrefs.getBoolean(\n                \"search_area_bangumi\",\n                false\n            ) || sPrefs.getBoolean(\"search_area_movie\", false))\n        ) {\n            val currentArea = runCatchingOrNull {\n                XposedInit.country.get(5L, TimeUnit.SECONDS)\n            }\n            for (area in AREA_TYPES) {\n                if (area.value.area == currentArea)\n                    continue\n                if (!sPrefs.getString(area.value.area + \"_server\", null).isNullOrBlank() &&\n                    sPrefs.getBoolean(\"search_area_\" + area.value.typeStr, false)\n                ) {\n                    searchAllResultNavInfoClass?.new()?.run {\n                        setObjectField(\"name\", area.value.text)\n                        setIntField(\"pages\", 0)\n                        setIntField(\"total\", 0)\n                        setIntField(\"type\", area.key)\n                    }?.also {\n                        body.getObjectFieldAs<MutableList<Any>>(\"nav\").add(1, it)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun fixPlaySearchType(body: Any, url: String) {\n        val dataField =\n            if (instance.generalResponseClass?.isInstance(body) == true) \"data\" else instance.responseDataField()\n        val resultClass = body.getObjectField(dataField)?.javaClass ?: return\n        if (!url.contains(\"type=7\") && !url.contains(\"type=8\")) return\n        val newUrl = url.replace(\"appintl.biliapi.net/intl/gateway/app/\", \"app.bilibili.com/x/v2/\")\n        val content = getContent(newUrl)?.toJSONObject()?.optJSONObject(\"data\") ?: return\n        val newResult = resultClass.fromJson(content) ?: return\n        body.setObjectField(dataField, newResult)\n    }\n\n    private fun fixBangumi(jsonResult: JSONObject?, code: Int, url: String?) =\n        if (isBangumiWithWatchPermission(jsonResult, code)) {\n            BangumiPlayUrlHook.qnApplied.set(false)\n            jsonResult?.also { allowDownload(it); fixEpisodesStatus(it) }\n        } else {\n            BangumiPlayUrlHook.qnApplied.set(false)\n            url?.let { Uri.parse(it) }?.run {\n                getQueryParameter(\"ep_id\")?.let {\n                    lastSeasonInfo.clear()\n                    lastSeasonInfo[\"ep_id\"] = it\n                }\n                getQueryParameter(\"season_id\")?.let {\n                    lastSeasonInfo.clear()\n                    lastSeasonInfo[\"season_id\"] = it\n                }\n            }\n            Log.toast(\"发现区域限制番剧，尝试解锁……\")\n            Log.d(\"Info: $lastSeasonInfo\")\n            val (newCode, newJsonResult) = getSeason(\n                lastSeasonInfo,\n                jsonResult\n            )?.toJSONObject()?.let {\n                it.optInt(\"code\", FAIL_CODE) to it.optJSONObject(\"result\")\n            } ?: (FAIL_CODE to null)\n            if (isBangumiWithWatchPermission(newJsonResult, newCode)) {\n                lastSeasonInfo[\"title\"] = newJsonResult?.optString(\"title\")\n                lastSeasonInfo[\"season_id\"] = newJsonResult?.optString(\"season_id\")\n                lastSeasonInfo[\"watch_platform\"] =\n                    newJsonResult?.optJSONObject(\"rights\")?.apply {\n                        if (has(\"allow_comment\") && getInt(\"allow_comment\") == 0) {\n                            remove(\"allow_comment\")\n                            put(\"area_limit\", 1)\n                            lastSeasonInfo[\"allow_comment\"] = \"0\"\n                        }\n                    }?.optInt(\"watch_platform\")?.toString()\n                for (episode in newJsonResult?.optJSONArray(\"episodes\").orEmpty()) {\n                    if (episode.has(\"cid\") && episode.has(\"id\")) {\n                        val cid = episode.optInt(\"cid\").toString()\n                        val epId = episode.optInt(\"id\").toString()\n                        lastSeasonInfo[cid] = epId\n                        lastSeasonInfo[\"ep_ids\"] = lastSeasonInfo[\"ep_ids\"]?.let { \"$it;$epId\" }\n                            ?: epId\n                        episode.optJSONArray(\"subtitles\")?.let {\n                            if (it.length() > 0) {\n                                lastSeasonInfo[\"area\"] = \"th\"\n                                lastSeasonInfo[\"sb$cid\"] = it.toString()\n                            }\n                        }\n                    }\n                }\n                allowDownload(newJsonResult, false)\n                fixEpisodesStatus(newJsonResult)\n            }\n            jsonResult?.apply {\n                // Replace only episodes and rights\n                // Remain user information, such as follow status, watch progress, etc.\n                put(\"rights\", newJsonResult?.optJSONObject(\"rights\"))\n                put(\"episodes\", newJsonResult?.optJSONArray(\"episodes\"))\n                remove(\"limit\")\n                put(\"modules\", newJsonResult?.optJSONArray(\"modules\"))\n                put(\"section\", newJsonResult?.optJSONArray(\"section\"))\n                put(\"prevueSection\", newJsonResult?.optJSONArray(\"prevueSection\"))\n                remove(\"dialog\")\n            } ?: newJsonResult\n        }\n\n    private fun fixBangumi(body: Any, url: String?) {\n        val fieldName = \"_data\"\n        //if (isSerializable || isGson) instance.responseDataField().value else \"result\"\n        val result = body.getObjectField(fieldName)\n        val code = body.getIntField(\"code\")\n        if (instance.bangumiUniformSeasonClass?.isInstance(result) != true && code != FAIL_CODE) return\n        if (url?.contains(\"cards?\") == true) return\n        val jsonResult = result?.toJson()\n        // Filter normal bangumi and other responses\n        fixBangumi(jsonResult, code, url)?.let {\n            body.setIntField(\"code\", 0)\n                .setObjectField(fieldName, instance.bangumiUniformSeasonClass?.fromJson(it))\n        } ?: run {\n            lastSeasonInfo.clear()\n        }\n    }\n\n    private fun fixViewProto(req: ViewReq): ViewReply? {\n        val av = when {\n            req.hasAid() -> req.aid.toString()\n            req.hasBvid() -> bv2av(req.bvid).toString()\n            else -> return null\n        }\n        val query = Uri.Builder().run {\n            appendQueryParameter(\"id\", av)\n            appendQueryParameter(\"bvid\", req.bvid.toString())\n            appendQueryParameter(\"from\", req.from.toString())\n            appendQueryParameter(\"trackid\", req.trackid.toString())\n            appendQueryParameter(\"ad_extra\", req.adExtra.toString())\n            appendQueryParameter(\"qn\", req.qn.toString())\n            appendQueryParameter(\"fnver\", req.fnver.toString())\n            appendQueryParameter(\"fnval\", req.fnval.toString())\n            appendQueryParameter(\"force_host\", req.forceHost.toString())\n            appendQueryParameter(\"fourk\", req.fourk.toString())\n            appendQueryParameter(\"spmid\", req.spmid.toString())\n            appendQueryParameter(\"autoplay\", req.autoplay.toString())\n            build()\n        }.query\n\n        BiliRoamingApi.getPagelist(av) ?: return null\n\n        Log.toast(\"发现区域限制视频，尝试解锁……\")\n\n        val content = BiliRoamingApi.getView(query)?.toJSONObject() ?: return null\n        val result = content.optJSONObject(\"v2_app_api\") ?: return null\n\n        return viewReply {\n            arc = arc {\n                author = author {\n                    result.optJSONObject(\"owner\")?.run {\n                        mid = optLong(\"mid\")\n                        face = optString(\"face\")\n                        name = optString(\"name\")\n                    }\n                }\n                dimension = dimension {\n                    result.optJSONObject(\"dimension\")?.run {\n                        width = optLong(\"width\")\n                        height = optLong(\"height\")\n                        rotate = optLong(\"rotate\")\n                    }\n                }\n                stat = stat {\n                    result.optJSONObject(\"stat\")?.run {\n                        aid = optLong(\"aid\")\n                        view = optInt(\"view\")\n                        danmaku = optInt(\"danmaku\")\n                        reply = optInt(\"reply\")\n                        fav = optInt(\"favorite\")\n                        coin = optInt(\"coin\")\n                        share = optInt(\"share\")\n                        nowRank = optInt(\"now_rank\")\n                        hisRank = optInt(\"his_rank\")\n                        like = optInt(\"like\")\n                        dislike = optInt(\"dislike\")\n                    }\n                }\n                result.run {\n                    aid = optLong(\"aid\")\n                    attribute = optInt(\"attribute\")\n                    copyright = optInt(\"copyright\")\n                    ctime = optLong(\"ctime\")\n                    desc = optString(\"desc\")\n                    duration = optLong(\"duration\")\n                    firstCid = optLong(\"cid\")\n                    pic = optString(\"pic\")\n                    pubdate = optLong(\"pubdate\")\n                    redirectUrl = optString(\"redirect_url\")\n                    state = optInt(\"state\")\n                    title = optString(\"title\")\n                    typeId = optInt(\"tid\")\n                    typeName = optString(\"tname\")\n                    videos = optLong(\"videos\")\n                }\n                rights = rights {\n                    result.optJSONObject(\"rights\")?.run {\n                        bp = optInt(\"bp\")\n                        elec = optInt(\"elec\")\n                        download = if (needUnlockDownload) 1 else optInt(\"download\")\n                        movie = optInt(\"movie\")\n                        pay = optInt(\"pay\")\n                        hd5 = optInt(\"hd5\")\n                        noReprint = optInt(\"no_reprint\")\n                        autoplay = optInt(\"autoplay\")\n                        isCooperation = optInt(\"is_cooperation\")\n                        ugcPay = optInt(\"ugc_pay\")\n                        noBackground = if (sPrefs.getBoolean(\"play_arc_conf\", false))\n                            0 else optInt(\"no_background\")\n                    }\n                }\n            }\n            bvid = result.optString(\"bvid\")\n            val pages = result.optJSONArray(\"pages\")\n            for (page in pages.orEmpty()) {\n                this.pages += viewPage {\n                    downloadSubtitle = page.optString(\"download_subtitle\")\n                    downloadTitle = page.optString(\"download_title\")\n                    this.page = page {\n                        this.page = page.optInt(\"page\")\n                        page.run {\n                            cid = optLong(\"cid\")\n                            from = optString(\"from\")\n                            part = optString(\"part\")\n                            duration = optLong(\"duration\")\n                            vid = optString(\"vid\")\n                            webLink = optString(\"weblink\")\n                        }\n                        dimension = dimension {\n                            page.optJSONObject(\"dimension\")?.run {\n                                width = optLong(\"width\")\n                                height = optLong(\"height\")\n                                rotate = optLong(\"rotate\")\n                            }\n                        }\n                    }\n                }\n            }\n            shortLink = result.optString(\"short_link\")\n            result.optJSONObject(\"t_icon\")?.let {\n                for (key in it.keys()) {\n                    this.tIcon[key] = tIcon {\n                        icon = it.optJSONObject(key)?.optString(\"icon\") ?: \"\"\n                    }\n                }\n            }\n            val tags = result.optJSONArray(\"tag\")\n            for (tag in tags.orEmpty()) {\n                this.tag += tag {\n                    tag.run {\n                        id = optLong(\"tag_id\")\n                        name = optString(\"tag_name\")\n                        likes = optLong(\"likes\")\n                        liked = optInt(\"liked\")\n                        hates = optLong(\"hates\")\n                        hated = optInt(\"hated\")\n                        tagType = optString(\"tag_type\")\n                        uri = optString(\"uri\")\n                    }\n                }\n            }\n            ownerExt = ownerExt {\n                result.optJSONObject(\"owner_ext\")?.run {\n                    officialVerify = officialVerify {\n                        optJSONObject(\"official_verify\")?.run {\n                            type = optInt(\"type\")\n                            desc = optString(\"desc\")\n                        }\n                    }\n                    vip = vip {\n                        optJSONObject(\"vip\")?.run {\n                            vipType = optInt(\"vipType\")\n                            dueDate = optLong(\"vipDueDate\")\n                            dueRemark = optString(\"dueRemark\")\n                            accessStatus = optInt(\"accessStatus\")\n                            vipStatus = optInt(\"vipStatus\")\n                            vipStatusWarn = optString(\"vipStatusWarn\")\n                            themeType = optInt(\"themeType\")\n                            label = vipLabel {\n                                optJSONObject(\"label\")?.run {\n                                    path = optString(\"path\")\n                                    text = optString(\"text\")\n                                    labelTheme = optString(\"label_theme\")\n                                }\n                            }\n                        }\n                    }\n                    fans = optLong(\"fans\")\n                    arcCount = optString(\"arc_count\")\n                }\n            }\n            config = config {\n                result.optJSONObject(\"config\")?.run {\n                    relatesTitle = optString(\"relates_title\")\n                    abtestSmallWindow = optString(\"abtest_small_window\")\n                    recThreePointStyle = optInt(\"rec_three_point_style\")\n                    isAbsoluteTime = optBoolean(\"is_absolute_time\")\n                    relatesFeedStyle = optString(\"feed_style\")\n                    relatesFeedHasNext = optBoolean(\"feed_has_next\")\n                    localPlay = optInt(\"local_play\")\n                }\n            }\n        }\n    }\n\n    private fun fixViewProto(req: ViewUniteReq, av: String?): ViewUniteReply? {\n        av ?: return fixViewProto(req)\n\n        Log.toast(\"发现区域限制视频，尝试解锁……\")\n        val query = Uri.Builder().run {\n            appendQueryParameter(\"id\", av)\n            appendQueryParameter(\"bvid\", req.bvid.toString())\n            appendQueryParameter(\"from\", req.from.toString())\n            appendQueryParameter(\"trackid\", req.trackId.toString())\n            appendQueryParameter(\"ad_extra\", req.adExtra.toString())\n            appendQueryParameter(\"qn\", req.playerArgs.qn.toString())\n            appendQueryParameter(\"fnver\", req.playerArgs.fnver.toString())\n            appendQueryParameter(\"fnval\", req.playerArgs.fnval.toString())\n            appendQueryParameter(\"force_host\", req.playerArgs.forceHost.toString())\n            appendQueryParameter(\"spmid\", req.spmid.toString())\n            build()\n        }.query\n\n        BiliRoamingApi.getPagelist(av) ?: return null\n\n        Log.toast(\"发现区域限制视频，尝试解锁……\")\n\n        val content = BiliRoamingApi.getView(query)?.toJSONObject() ?: return null\n        val result = content.optJSONObject(\"v2_app_api\") ?: return null\n        Log.w(result)\n        return viewUniteReply {\n            arc = viewUniteArc {\n                stat = viewUniteStat {\n                    result.optJSONObject(\"stat\")?.run {\n                        reply = optLong(\"reply\")\n                        fav = optLong(\"favorite\")\n                        coin = optLong(\"coin\")\n                        share = optLong(\"share\")\n                        like = optLong(\"like\")\n                    }\n                }\n                result.run {\n                    aid = optLong(\"aid\")\n                    copyright = optInt(\"copyright\")\n                    duration = optLong(\"duration\")\n                    title = optString(\"title\")\n                    typeId = optLong(\"tid\")\n                    cover = optString(\"pic\")\n                    bvid = optString(\"bvid\")\n                    cid = optLong(\"cid\")\n                }\n                right = viewUniteArcRights {\n                    download = true\n                    onlyVipDownload = true\n                }\n            }\n            supplement = any {\n                typeUrl = UGC_ANY_MODEL_TYPE_URL\n                value = viewUgcAny {\n                    shareSubtitle = result.optString(\"share_subtitle\")\n                    shortLink = result.optString(\"short_link\")\n\n                    val pages = result.optJSONArray(\"pages\")\n                    for (page in pages.orEmpty()) {\n                        this.pages += ugcPage {\n                            page.run {\n                                dlSubtitle = optString(\"download_subtitle\")\n                                dlTitle = optString(\"download_title\")\n                                cid = optLong(\"cid\")\n                                part = optString(\"part\")\n                                duration = optLong(\"duration\")\n                                dimension = ugcDimension {\n                                    optJSONObject(\"dimension\")?.run {\n                                        width = optLong(\"width\")\n                                        height = optLong(\"height\")\n                                        rotate = optLong(\"rotate\")\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }.toByteString()\n            }\n            viewBase = viewBase {\n                bizType = 1\n            }\n            tab = tab {\n                tabModule += tabModule {\n                    tabType = TabType.TAB_INTRODUCTION\n                    introduction = introductionTab {\n                        title = \"简介\"\n                        modules += module {\n                            type = ModuleType.OWNER\n                        }\n                        modules += module {\n                            type = ModuleType.UGC_HEADLINE\n                            headLine = headline {\n                                this.content = result.optString(\"title\")\n                            }\n                        }\n                        modules += module {\n                            type = ModuleType.UGC_INTRODUCTION\n                            ugcIntroduction = ugcIntroduction {\n                                desc += descV2 {\n                                    text = result.optString(\"desc\")\n                                    type = DescType.DescTypeText\n                                }\n                                pubdate = result.optLong(\"ctime\")\n                                val tags = result.optJSONArray(\"tag\")\n                                for (tag in tags.orEmpty()) {\n                                    this.tags += ugcTag {\n                                        tag.run {\n                                            id = optLong(\"tag_id\")\n                                            name = optString(\"tag_name\")\n                                            tagType = optString(\"tag_type\")\n                                            uri = optString(\"uri\")\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        addKingPosition()\n                    }\n                }\n                tabModule += tabModule {\n                    tabType = TabType.TAB_REPLY\n                    reply = replyTab {\n                        title = \"评论\"\n                        replyStyle = replyStyle {\n                            badgeType = 0L\n                        }\n                    }\n                }\n            }\n            owner = viewUniteOwner {\n                result.optJSONObject(\"owner_ext\")?.run {\n                    officialVerify = viewUniteOfficialVerify {\n                        optJSONObject(\"official_verify\")?.run {\n                            type = optInt(\"type\")\n                            desc = optString(\"desc\")\n                        }\n                    }\n                    vip = viewUniteVip {\n                        optJSONObject(\"vip\")?.run {\n                            type = optInt(\"vipType\")\n                            isVip = if (optInt(\"vipStatus\") != 0) 1 else 0\n                            status = optInt(\"vipStatus\")\n                            themeType = optInt(\"themeType\")\n                            vipLabel = viewUniteVipLabel {\n                                optJSONObject(\"label\")?.run {\n                                    path = optString(\"path\")\n                                    text = optString(\"text\")\n                                    labelTheme = optString(\"label_theme\")\n                                }\n                            }\n                        }\n                    }\n                    fans = optLong(\"fans\").toString()\n                    arcCount = optString(\"arc_count\")\n                }\n                result.optJSONObject(\"owner\")?.run {\n                    title = optString(\"name\")\n                    face = optString(\"face\")\n                    mid = optLong(\"mid\")\n                }\n            }\n        }\n    }\n    private fun fixViewProto(resp: Any, supplement: ViewPgcAny) {\n        val isAreaLimit = supplement.ogvData.rights.areaLimit != 0\n\n        if (!(isAreaLimit || needUnlockDownload)) return\n\n        if (isAreaLimit) Log.toast(\"发现区域限制视频，尝试解锁……\")\n\n        resp.callMethod(\"getArc\")?.callMethod(\"getRight\")?.run {\n            callMethod(\"setDownload\", true)\n            callMethod(\"setOnlyVipDownload\", false)\n            callMethod(\"setNoReprint\", false)\n        }\n\n        val newSupplement = supplement.copy {\n            ogvData = ogvData.copy {\n                rights = rights.copy {\n                    if (needUnlockDownload) {\n                        allowDownload = 1\n                        onlyVipDownload = 0\n                        newAllowDownload = 1\n                    }\n                    allowReview = 1\n                    areaLimit = 0\n                    banAreaShow = 0\n                }\n            }\n        }\n        \"com.google.protobuf.Any\".from(mClassLoader)?.callStaticMethod(\n            \"parseFrom\", any {\n                typeUrl = PGC_ANY_MODEL_TYPE_URL\n                value = newSupplement.toByteString()\n            }.toByteArray()\n        )?.let {\n            resp.callMethod(\"setSupplement\", it)\n        }\n\n        val tab = resp.callMethod(\"getTab\") ?: return\n        val newTab = tab.callMethodAs<ByteArray>(\"toByteArray\").let {\n            Tab.parseFrom(it)\n        }.copy {\n            val newTabModule = tabModule.map { tabModule ->\n                 tabModule.copy tab_copy@ {\n                    if (!hasIntroduction()) return@tab_copy\n                    introduction = introduction.copy {\n                        val newModules = modules.map { module ->\n                            module.copy module_copy@ {\n                                if (!hasSectionData()) return@module_copy\n                                sectionData = sectionData.copy {\n                                    val newEpisodes = episodes.map {\n                                        it.copy {\n                                            badgeInfo = badgeInfo.copy {\n                                                if (text == \"受限\") {\n                                                    text = \"\"\n                                                }\n                                            }\n                                            rights = rights.copy {\n                                                if (needUnlockDownload) {\n                                                    allowDownload = 1\n                                                }\n                                                allowReview = 1\n                                                canWatch = 1\n                                                allowDm = 1\n                                                allowDemand = 1\n                                                areaLimit = 0\n                                            }\n                                        }\n                                    }\n                                    episodes.clear()\n                                    episodes.addAll(newEpisodes)\n                                }\n                            }\n                        }\n                        modules.clear()\n                        modules.addAll(newModules)\n                    }\n                }\n            }\n            tabModule.clear()\n            tabModule.addAll(newTabModule)\n        }\n        tab.javaClass.callStaticMethod(\"parseFrom\", newTab.toByteArray())?.let {\n            resp.callMethod(\"setTab\", it)\n        }\n    }\n\n    private fun IntroductionTabKt.Dsl.addKingPosition() {\n        modules += module {\n            type = ModuleType.KING_POSITION\n            kingPosition = kingPosition {\n                kingPos += kingPos {\n                    type = KingPos.KingPositionType.LIKE\n                }\n                kingPos += kingPos {\n                    type = KingPos.KingPositionType.COIN\n                }\n                kingPos += kingPos {\n                    type = KingPos.KingPositionType.FAV\n                }\n                kingPos += kingPos {\n                    type = KingPos.KingPositionType.CACHE\n                }\n                kingPos += kingPos {\n                    type = KingPos.KingPositionType.SHARE\n                }\n            }\n        }\n    }\n\n    private fun fixViewProto(req: ViewUniteReq): ViewUniteReply? {\n        Log.toast(\"发现东南亚区域番剧，尝试解锁……\")\n        val reqEpId = req.extraContentMap[\"ep_id\"]?.also {\n            lastSeasonInfo.clear()\n            lastSeasonInfo[\"ep_id\"] = it\n        } ?: \"0\"\n        val reqSeasonId = req.extraContentMap[\"season_id\"]?.also {\n            lastSeasonInfo.clear()\n            lastSeasonInfo[\"season_id\"] = it\n        } ?: \"0\"\n\n        val seasonInfo = getSeason(mapOf(\"season_id\" to reqSeasonId, \"ep_id\" to reqEpId), null)?.toJSONObject()?.let {\n            val eCode = it.optLong(\"code\")\n            if (eCode != 0L) {\n                Log.e(\"Invalid thai season info reply, code $eCode, message ${it.optString(\"message\")}\")\n                return null\n            }\n            it.optJSONObject(\"result\")\n        } ?: return null\n\n        val seasonId = seasonInfo.optString(\"season_id\") ?: return null\n        lastSeasonInfo[\"title\"] = seasonInfo.optString(\"title\")\n        lastSeasonInfo[\"season_id\"] = seasonId\n\n        val viewBaseDefault = viewBase {\n            bizType = 2\n            pageType = ViewBase.PageType.H5\n        }\n\n        val arcDefault = viewUniteArc {\n            copyright = 1\n            right = viewUniteArcRights {\n                download = true\n                onlyVipDownload = false\n            }\n        }\n\n        val introductionTab = introductionTab {\n            title = \"简介\"\n            modules += module {\n                type = ModuleType.OGV_TITLE\n                ogvTitle = ogvTitle {\n                    title = seasonInfo.optString(\"title\")\n                    reserveId = 0\n                }\n            }\n            modules += module {\n                type = ModuleType.OGV_INTRODUCTION\n                ogvIntroduction = ogvIntroduction {\n                    followers =\n                        seasonInfo.optJSONObject(\"stat\")?.optString(\"followers\") ?: \"0.00 万\"\n                    playData = statInfo {\n                        icon = \"playdata-square-line@500\"\n                        pureText = seasonInfo.optJSONObject(\"stat_format\")?.optString(\"play\") ?: \"\"\n                        text = pureText.replace(\"播放\", \"\")\n                        value = seasonInfo.optJSONObject(\"stat\")?.optLong(\"views\") ?: 0\n                    }\n                }\n            }\n            addKingPosition()\n\n            // seasons\n            seasonInfo.optJSONObject(\"series\")?.optJSONArray(\"seasons\")?.takeIf { it.length() > 0 }\n                ?.let { seasonArray ->\n                    modules += module {\n                        type = ModuleType.OGV_SEASONS\n                        ogvSeasons = ogvSeasons {\n                            seasonArray.iterator().forEach { season ->\n                                serialSeason {\n                                    this.seasonId = season.optInt(\"season_id\")\n                                    seasonTitle = season.optString(\"quarter_title\")\n                                }.let {\n                                    this.serialSeason.add(it)\n                                }\n                            }\n                        }\n                    }\n                }\n\n            val reconstructSectionData = { module: JSONObject ->\n                sectionData {\n                    id = module.optInt(\"id\")\n                    moduleStyle = style {\n                        module.optJSONObject(\"module_style\")?.run {\n                            hidden = optInt(\"hidden\")\n                            line = optInt(\"line\")\n                        }\n                    }\n                    more = module.optString(\"more\")\n                    sectionId = module.optJSONObject(\"data\")?.optInt(\"id\") ?: 0\n                    title = module.optString(\"title\")\n                    module.optJSONObject(\"data\")?.optJSONArray(\"episodes\")?.iterator()\n                        ?.forEach { episode ->\n                            viewEpisode {\n                                aid = episode.optLong(\"aid\")\n                                badgeInfo = badgeInfo {\n                                    episode.optJSONObject(\"badge_info\")?.run {\n                                        bgColor = optString(\"bg_color\")\n                                        bgColorNight = optString(\"bg_color_night\")\n                                        text = optString(\"text\")\n                                    }\n                                }\n                                cid = episode.optLong(\"cid\")\n                                cover = episode.optString(\"cover\")\n                                dimension = dimension {\n                                    episode.optJSONObject(\"dimension\")?.run {\n                                        width = optLong(\"width\")\n                                        rotate = optLong(\"rotate\")\n                                        height = optLong(\"height\")\n                                    }\n                                }\n                                duration = episode.optInt(\"duration\")\n                                epId = episode.optLong(\"id\")\n                                epIndex = episode.optInt(\"index\")\n                                from = episode.optString(\"from\")\n                                link = episode.optString(\"link\")\n                                longTitle = episode.optString(\"long_title\")\n                                rights = viewEpisodeRights {\n                                    val rights = episode.optJSONObject(\"rights\")\n                                    allowDemand = rights?.optInt(\"allow_demand\") ?: 1\n                                    allowDm = rights?.optInt(\"allow_dm\") ?: 0\n                                    allowDownload = rights?.optInt(\"allow_download\") ?: 0\n                                    areaLimit = rights?.optInt(\"area_limit\") ?: 1\n                                    if (needUnlockDownload) {\n                                        allowDownload = 1\n                                    }\n                                }\n                                sectionIndex = episode.optInt(\"section_index\")\n                                shareUrl = episode.optString(\"share_url\")\n                                statForUnity = viewEpisodeStat {}\n                                status = episode.optInt(\"status\")\n                                title = episode.optString(\"title\")\n                                if (!sPrefs.getString(\"cn_server_accessKey\", null)\n                                        .isNullOrEmpty()\n                                ) {\n                                    if (status == 13) status = 2\n                                }\n                            }.let { episodes.add(it) }\n                            if (episode.has(\"cid\") && episode.has(\"id\")) {\n                                val cid = episode.optInt(\"cid\").toString()\n                                val epId = episode.optInt(\"id\").toString()\n                                lastSeasonInfo[cid] = epId\n                                lastSeasonInfo[\"ep_ids\"] =\n                                    lastSeasonInfo[\"ep_ids\"]?.let { \"$it;$epId\" } ?: epId\n                            }\n                        }\n                }\n            }\n            // episodes\n            seasonInfo.optJSONArray(\"modules\")?.iterator()?.forEach { module ->\n                val style = module.optString(\"style\")\n                if (style == \"positive\") {\n                    modules += module {\n                        type = ModuleType.POSITIVE\n                        sectionData = reconstructSectionData(module)\n                    }\n                } else {\n                    modules += module {\n                        type = ModuleType.SECTION\n                        sectionData = reconstructSectionData(module)\n                    }\n                }\n            }\n        }\n        val tabDefault = tab {\n            tabModule += tabModule {\n                tabType = TabType.TAB_INTRODUCTION\n                introduction = introductionTab\n            }\n            tabModule += tabModule {\n                tabType = TabType.TAB_REPLY\n                reply = replyTab {\n                    replyStyle = replyStyle {\n                        badgeType = 0L\n                    }\n                    title = \"评论\"\n                }\n            }\n        }\n\n        val viewPgcAny = viewPgcAny {\n            ogvData = ogvData {\n                aid = 0\n                cover = seasonInfo.optString(\"cover\")\n                    ?: \"https://i1.hdslb.com/bfs/archive/5242750857121e05146d5d5b13a47a2a6dd36e98.jpg\"\n                horizontalCover169 = seasonInfo.optString(\"horizon_cover\")\n                hasCanPlayEp =\n                    if (seasonInfo.optJSONArray(\"episodes\").orEmpty().length() > 0) 1 else 0\n                mediaId = seasonInfo.optInt(\"season_id\")\n                mode = 2\n                newEp = newEp {\n                    seasonInfo.optJSONObject(\"new_ep\")?.run {\n                        desc = optString(\"new_ep_display\")\n                        id = optInt(\"id\")\n                        title = optString(\"title\")\n                    }\n                }\n                ogvSwitch = ogvSwitch {\n                    mergePreviewSection = 1\n                }\n                playStrategy = playStrategy {\n                    autoPlayToast = \"即将播放\"\n                    recommendShowStrategy = 1\n                    strategies.addAll(\n                        listOf(\n                            \"common_section-formal_first_ep\",\n                            \"common_section-common_section\",\n                            \"common_section-next_season\",\n                            \"formal-finish-next_season\",\n                            \"formal-end-other_section\",\n                            \"formal-end-next_season\",\n                            \"ord\"\n                        )\n                    )\n                }\n                publish = publish {\n                    seasonInfo.optJSONObject(\"publish\")?.run {\n                        isFinish = optInt(\"is_finish\")\n                        isStarted = optInt(\"is_started\")\n                        pubTime = optString(\"pub_time\")\n                        pubTimeShow = optString(\"pub_time_show\")\n                        releaseDateShow = optString(\"release_date_show\")\n                        timeLengthShow = optString(\"time_length_show\")\n                        unknowPubDate = optInt(\"unknow_pub_date\")\n                        weekday = optInt(\"weekday\")\n                    }\n                }\n                rights = ogvDataRights {\n                    seasonInfo.optJSONObject(\"rights\")?.run {\n                        allowBp = optInt(\"allow_bp\")\n                        allowBpRank = optInt(\"allow_bp_rank\")\n                        allowReview = optInt(\"allow_review\")\n                        areaLimit = 0\n                        banAreaShow = optInt(\"ban_area_show\")\n                        canWatch = 1\n                        copyright = optString(\"copyright\")\n                        forbidPre = optInt(\"forbidPre\")\n                        isPreview = optInt(\"is_preview\")\n                        onlyVipDownload = optInt(\"onlyVipDownload\")\n                        if (has(\"allow_comment\") && getInt(\"allow_comment\") == 0) {\n                            areaLimit = 1\n                            // To be honest, Thai video's comment area (also called tab)\n                            // will be removed entirely if not set to force enable it\n                            lastSeasonInfo[\"allow_comment\"] = \"0\"\n                        }\n                    }\n                    if (needUnlockDownload) {\n                        allowDownload = 1\n                        newAllowDownload = 1\n                        onlyVipDownload = 0\n                    }\n                }\n                this.seasonId = seasonInfo.optLong(\"season_id\")\n                seasonType = seasonInfo.optInt(\"type\")\n                shareUrl = seasonInfo.optString(\"share_url\")\n                shortLink = seasonInfo.optString(\"short_link\")\n                showSeasonType = seasonInfo.optInt(\"type\")\n                squareCover = seasonInfo.optString(\"square_cover\")\n                stat = ogvDataStat {\n                    followers =\n                        seasonInfo.optJSONObject(\"stat\")?.optString(\"followers\") ?: \"0.00 万\"\n                    playData = statInfo {\n                        icon = \"playdata-square-line@500\"\n                        pureText = seasonInfo.optJSONObject(\"stat_format\")?.optString(\"play\")\n                            ?: \"0.00 万播放\"\n                        text = pureText.replace(\"播放\", \"\")\n                        value = seasonInfo.optJSONObject(\"stat\")?.optLong(\"views\") ?: 0\n                    }\n                }\n                status = seasonInfo.optInt(\"status\")\n                if (!sPrefs.getString(\"cn_server_accessKey\", null).isNullOrEmpty()) {\n                    if (status == 13) status = 2\n                }\n                title = seasonInfo.optString(\"title\")\n                userStatus = ogvDataUserStatus {\n                    seasonInfo.optJSONObject(\"user_status\")?.run {\n                        follow = optInt(\"follow\")\n                        vip = optInt(\"vip\")\n                    }\n                }\n            }\n        }\n        val supplementAny = any {\n            typeUrl = PGC_ANY_MODEL_TYPE_URL\n            value = viewPgcAny.toByteString()\n        }\n\n        return viewUniteReply {\n            viewBase = viewBaseDefault\n            arc = arcDefault\n            tab = tabDefault\n            supplement = supplementAny\n        }\n    }\n\n    private fun fixView(data: Any?, urlString: String): Any? {\n        val uri = Uri.parse(urlString)\n        val av = uri.getQueryParameter(\"aid\")?.takeIf {\n            it != \"0\"\n        } ?: uri.getQueryParameter(\"bvid\")?.let {\n            bv2av(it).toString()\n        } ?: return null\n        val queryString = uri.encodedQuery + \"&id=$av\"\n        val content = BiliRoamingApi.getView(queryString) ?: return data\n        val detailClass = instance.biliVideoDetailClass ?: return data\n        val newJsonResult = content.toJSONObject().optJSONObject(\"v2_app_api\") ?: return data\n        newJsonResult.optJSONObject(\"season\")?.optString(\"newest_ep_id\")?.let {\n            lastSeasonInfo[\"ep_id\"] = it\n        }\n        return detailClass.fromJson(newJsonResult)\n    }\n\n    private fun isBangumiWithWatchPermission(result: JSONObject?, code: Int) = result?.let {\n        result.optJSONObject(\"rights\")\n            ?.run { !optBoolean(\"area_limit\", true) || optInt(\"area_limit\", 1) == 0 }\n    } ?: run { code != FAIL_CODE }\n\n\n    private fun allowDownload(result: JSONObject?, toast: Boolean = true) {\n        if (needUnlockDownload) {\n            val rights = result?.optJSONObject(\"rights\")\n            rights?.put(\"allow_download\", 1)\n            rights?.put(\"only_vip_download\", 0)\n            for (module in result?.optJSONArray(\"modules\").orEmpty()) {\n                val data = module.optJSONObject(\"data\")\n                val moduleEpisodes = data?.optJSONArray(\"episodes\")\n                for (episode in moduleEpisodes.orEmpty()) {\n                    episode.optJSONObject(\"rights\")?.put(\"allow_download\", 1)\n                }\n            }\n            for (section in result?.optJSONArray(\"prevueSection\").orEmpty()) {\n                for (episode in section.optJSONArray(\"episodes\").orEmpty()) {\n                    episode.optJSONObject(\"rights\")?.put(\"allow_download\", 1)\n                }\n            }\n            for (episode in result?.optJSONArray(\"episodes\").orEmpty()) {\n                episode.optJSONObject(\"rights\")?.put(\"allow_download\", 1)\n            }\n            Log.d(\"Download allowed\")\n            if (toast) Log.toast(\"已允许下载\")\n        }\n    }\n\n    private fun fixEpisodesStatus(result: JSONObject?) {\n        sPrefs.getString(\"cn_server_accessKey\", null) ?: return\n        if (result?.optInt(\"status\") == 13) result.put(\"status\", 2)\n        for (module in result?.optJSONArray(\"modules\").orEmpty()) {\n            val data = module.optJSONObject(\"data\")\n            val moduleEpisodes = data?.optJSONArray(\"episodes\")\n            for (episode in moduleEpisodes.orEmpty()) {\n                if (episode.optInt(\"status\") == 13) episode.put(\"status\", 2)\n            }\n        }\n        for (section in result?.optJSONArray(\"prevueSection\").orEmpty()) {\n            for (episode in section.optJSONArray(\"episodes\").orEmpty()) {\n                if (episode.optInt(\"status\") == 13) episode.put(\"status\", 2)\n            }\n        }\n        for (episode in result?.optJSONArray(\"episodes\").orEmpty()) {\n            if (episode.optInt(\"status\") == 13) episode.put(\"status\", 2)\n        }\n    }\n\n    private fun setErrorMessage(activity: Activity) {\n        val job = MainScope().launch {\n            val id = getId(\"tv_desc\")\n            while (true) {\n                if (activity.isDestroyed) break\n                val tvDesc = activity.findViewById<TextView>(id)\n                if (tvDesc == null) {\n                    delay(500)\n                    continue\n                }\n                tvDesc.maxLines = Int.MAX_VALUE\n                (tvDesc.parent as View).setOnClickListener {\n                    val lines = tvDesc.text.lines()\n                    val title = lines[0]\n                    val message = lines.subList(1, lines.size).joinToString(\"\\n\").trim()\n                    AlertDialog.Builder(tvDesc.context)\n                        .setTitle(title)\n                        .setMessage(message)\n                        .setPositiveButton(android.R.string.ok, null)\n                        .show()\n                }\n                break\n            }\n        }\n        MainScope().launch {\n            delay(15_000)\n            job.cancel()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/BaseHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\n/**\n * Created by iAcn on 2019/3/27\n * Email i@iacn.me\n */\nabstract class BaseHook(val mClassLoader: ClassLoader) {\n    abstract fun startHook()\n    open fun lateInitHook() {}\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/BlockUpdateHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.content.Context\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.new\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass BlockUpdateHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"block_update\", false)) return\n        instance.updateInfoSupplierClass?.hookBeforeMethod(\n            instance.check(), Context::class.java\n        ) { param ->\n            val message = \"哼，休想要我更新！<(￣︶￣)>\"\n            param.throwable = instance.latestVersionExceptionClass?.new(message) as? Throwable\n                ?: Exception(message)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/CommentImageHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.content.ContentValues\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.Environment\nimport android.os.Parcelable\nimport android.provider.MediaStore\nimport android.view.HapticFeedbackConstants\nimport android.view.View\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.launch\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport java.io.File\nimport java.net.HttpURLConnection\nimport java.net.URL\n\nclass CommentImageHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    companion object {\n        fun saveImage(url: String) = runCatching {\n            URL(url).openStream().use { stream ->\n                val relativePath = \"${Environment.DIRECTORY_PICTURES}${File.separator}bili\"\n                val fullFilename = url.substringAfterLast('/')\n                val filename = fullFilename.substringBeforeLast('.')\n\n                val now = System.currentTimeMillis()\n                val contentValues = ContentValues().apply {\n                    put(MediaStore.MediaColumns.DISPLAY_NAME, filename)\n                    put(\n                        MediaStore.MediaColumns.MIME_TYPE,\n                        HttpURLConnection.guessContentTypeFromName(fullFilename) ?: \"image/png\"\n                    )\n                    put(MediaStore.MediaColumns.DATE_ADDED, now / 1000)\n                    put(MediaStore.MediaColumns.DATE_MODIFIED, now / 1000)\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                        put(MediaStore.MediaColumns.DATE_TAKEN, now)\n                        put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)\n                    } else {\n                        val path = File(\n                            Environment.getExternalStoragePublicDirectory(\n                                Environment.DIRECTORY_PICTURES\n                            ), \"bili\"\n                        ).also { it.mkdirs() }\n                        put(MediaStore.MediaColumns.DATA, File(path, fullFilename).absolutePath)\n                    }\n                }\n                val resolver = currentContext.contentResolver\n                val uri = resolver.insert(\n                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues\n                )\n                runCatching {\n                    resolver.openOutputStream(uri!!)?.use { stream.copyTo(it) }\n                }.onSuccess {\n                    Log.toast(\"图片已保存至\\n$relativePath${File.separator}$fullFilename\", true)\n                }.onFailure {\n                    Log.e(it)\n                    Log.toast(\"图片保存失败，可能已经保存或未授予权限\", true)\n                }\n            }\n        }.onFailure {\n            Log.e(it)\n            Log.toast(\"图片获取失败\", force = true)\n        }\n    }\n\n    private val imageViewId = getId(\"image_view\")\n    private var cacheUrlFieldName = \"\"\n\n    @Suppress(\"DEPRECATION\")\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"save_comment_image\", false)) return\n        instance.imageFragmentClass?.hookAfterMethod(\n            \"onViewCreated\", View::class.java, Bundle::class.java\n        ) { param ->\n            val self = param.thisObject\n            val view = param.args[0] as? View\n            val imageItem = self.callMethodOrNullAs<Bundle?>(\"getArguments\")\n                ?.getParcelable<Parcelable>(\"image_item\") ?: return@hookAfterMethod\n            val urlFieldName = cacheUrlFieldName.ifEmpty {\n                imageItem.javaClass.superclass.findFirstFieldByExactTypeOrNull(String::class.java)\n                    ?.name.orEmpty().also { cacheUrlFieldName = it }\n            }.ifEmpty { return@hookAfterMethod }\n            val imageUrl = imageItem.getObjectFieldAs<String?>(urlFieldName).takeIf {\n                !it.isNullOrEmpty() && it.startsWith(\"http\")\n            }?.substringBefore('@') ?: return@hookAfterMethod\n            view?.findViewById<View>(imageViewId)?.setOnLongClickListener {\n                it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)\n                MainScope().launch(Dispatchers.IO) {\n                    saveImage(imageUrl)\n                }\n                true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/CopyCommentHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.AlertDialog\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.view.View\nimport android.widget.TextView\nimport de.robv.android.xposed.XC_MethodHook\nimport de.robv.android.xposed.XposedHelpers\nimport me.iacn.biliroaming.utils.currentContext\nimport me.iacn.biliroaming.utils.getResId\nimport me.iacn.biliroaming.utils.sPrefs\n\n\nclass CopyCommentHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"copy_comment\", false)) return\n        XposedHelpers.findAndHookMethod(\n            \"com.bilibili.app.comment3.ui.widget.menu.CommentMoreMenuItemHolder\",\n            mClassLoader,\n            \"y3\",\n            \"kotlin.jvm.functions.Function1\",\n            \"com.bilibili.app.comment3.data.model.CommentItem\\$MenuItem\",\n            \"android.view.View\",  // ConstraintLayout\n            object : XC_MethodHook() {\n                override fun afterHookedMethod(param: MethodHookParam) {\n                    super.afterHookedMethod(param)\n                    val menu = param.args[1]\n                    val view = param.args[2] as View\n                    if (!menu.toString().contains(\"COPY\")) return\n\n                    val clipboard =\n                        currentContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                    // 实在是找不到了。那你就说有没有获取到吧\n                    val txt = clipboard.primaryClip!!.getItemAt(0).text\n                    AlertDialog.Builder(view.context, getResId(\"AppTheme.Dialog.Alert\", \"style\"))\n                        .run {\n                            setTitle(\"自由复制内容\")\n                            setMessage(txt)\n                            setPositiveButton(\"完成\") { _, _ -> }\n                            setNegativeButton(\"复制全部\") { _, _ -> }\n                            show()\n                        }.apply {\n                            findViewById<TextView>(android.R.id.message).setTextIsSelectable(true)\n                        }\n                }\n            })\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/CopyHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.content.Context\nimport android.content.Intent\nimport android.text.SpannableStringBuilder\nimport android.text.style.ClickableSpan\nimport android.view.View\nimport android.widget.FrameLayout\nimport android.widget.TextView\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport org.json.JSONObject\n\nclass CopyHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    companion object {\n        private val DYNAMIC_COPYABLE_IDS = arrayOf(\n            \"dy_card_text\",\n            \"dy_opus_paragraph_desc\",\n            \"dy_opus_paragraph_title\",\n            \"dy_opus_copy_right_id\",\n            \"dy_opus_paragraph_text\",\n        )\n    }\n\n    private val enhanceLongClickCopy = sPrefs.getBoolean(\"comment_copy_enhance\", false)\n\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"comment_copy\", false)) return\n        instance.descCopyView().zip(instance.descCopy()).forEach { p ->\n            val clazz = p.first ?: return@forEach\n            val method = p.second ?: return@forEach\n            clazz.replaceMethod(\n                method,\n                View::class.java,\n                ClickableSpan::class.java\n            ) { param ->\n                if (!enhanceLongClickCopy) return@replaceMethod Unit\n\n                param.thisObject.getFirstFieldByExactTypeOrNull<SpannableStringBuilder>()?.let {\n                    val view = param.args[0] as View\n                    showCopyDialog(view.context, it, param)\n                } ?: (param.args[0] as? TextView)?.let { tv ->\n                    showCopyDialog(tv.context, tv.text, param)\n                }\n            }\n        }\n\n        instance.dynamicDescHolderListeners().forEach { c ->\n            c?.replaceMethod(\"onLongClick\", View::class.java) { param ->\n                if (!enhanceLongClickCopy)\n                    return@replaceMethod true\n                val itemView = param.args[0] as? View\n                DYNAMIC_COPYABLE_IDS.asSequence().firstNotNullOfOrNull { n ->\n                    getId(n).takeIf { it != 0 }?.let { itemView?.findViewById<TextView>(it) }\n                }?.let { v ->\n                    (if (instance.ellipsizingTextViewClass?.isInstance(v) == true) {\n                        v.getFirstFieldByExactTypeOrNull()\n                    } else v.text)?.also { text ->\n                        showCopyDialog(v.context, text, param)\n                    }\n                } ?: Log.toast(\"找不到动态内容\", true)\n                true\n            }\n        }\n\n        val commentCopyHook = fun(param: MethodHookParam, idName: String): Any? {\n            if (!enhanceLongClickCopy) return true\n            if (param.args[0] is FrameLayout) return param.invokeOriginalMethod()\n            (param.args[0] as? View)?.findViewById<View>(getId(idName))?.let {\n                if (instance.commentSpanTextViewClass?.isInstance(it) == true ||\n                    instance.commentSpanEllipsisTextViewClass?.isInstance(it) == true\n                ) it else null\n            }?.let { view ->\n                view.getFirstFieldByExactTypeOrNull<CharSequence>()?.also { text ->\n                    showCopyDialog(view.context, text, param)\n                }\n            } ?: Log.toast(\"找不到评论内容\", true)\n            return true\n        }\n        instance.commentCopyClass?.replaceMethod(\"onLongClick\", View::class.java) {\n            commentCopyHook(it, \"message\")\n        }\n        instance.commentCopyNewClass?.replaceMethod(\"onLongClick\", View::class.java) {\n            commentCopyHook(it, \"comment_message\")\n        }\n\n        instance.comment3CopyClass?.let { c ->\n            instance.comment3Copy()?.let { m ->\n                instance.comment3ViewIndex().let { i ->\n                    c.replaceAllMethods(m) { param ->\n                        if (!enhanceLongClickCopy) return@replaceAllMethods true\n                        val view = param.args[i] as View\n                        view.getFirstFieldByExactTypeOrNull<CharSequence>()?.also { text ->\n                            showCopyDialog(view.context, text, param)\n                        }\n                        return@replaceAllMethods true\n                    }\n                }\n            }\n        }\n\n        if (!enhanceLongClickCopy) return\n        \"com.bilibili.bplus.im.conversation.ConversationActivity\".from(mClassLoader)\n            ?.declaredMethods?.find {\n                it.name == instance.onOperateClick() && it.parameterTypes.size == 8\n            }?.hookBeforeMethod { param ->\n                if (param.args.last() == param.args.first()) {\n                    val activity = param.thisObject as Activity\n                    val json = param.args[1].callMethodOrNullAs(instance.getContentString()) ?: \"\"\n                    val text = runCatchingOrNull { json.toJSONObject() }?.run {\n                        optString(\"content\").ifEmpty {\n                            buildString {\n                                appendLine(optString(\"title\").trim())\n                                appendLine(optString(\"text\").trim())\n                                optJSONArray(\"modules\")?.run {\n                                    asSequence<JSONObject>().map {\n                                        it.optString(\"title\") + \"：\" + it.optString(\"detail\")\n                                    }.joinToString(\"\\n\").run {\n                                        append(this)\n                                    }\n                                }\n                            }.run { removeSuffix(\"\\n\") }\n                        }\n                    } ?: return@hookBeforeMethod\n                    showCopyDialog(activity, text, param)\n                    param.args[6].callMethodOrNull(\"dismiss\")\n                    param.result = null\n                }\n            }\n    }\n\n    private fun showCopyDialog(context: Context, text: CharSequence, param: MethodHookParam) {\n        val appDialogTheme = getResId(\"AppTheme.Dialog.Alert\", \"style\")\n        AlertDialog.Builder(context, appDialogTheme).run {\n            setTitle(\"自由复制内容\")\n            setMessage(text)\n            setPositiveButton(\"分享\") { _, _ ->\n                context.startActivity(\n                    Intent.createChooser(\n                        Intent().apply {\n                            action = Intent.ACTION_SEND\n                            putExtra(Intent.EXTRA_TEXT, text)\n                            type = \"text/plain\"\n                        }, \"分享评论内容\"\n                    )\n                )\n            }\n            setNeutralButton(\"复制全部\") { _, _ ->\n                param.invokeOriginalMethod()\n            }\n            setNegativeButton(android.R.string.cancel, null)\n            show()\n        }.apply {\n            findViewById<TextView>(android.R.id.message).setTextIsSelectable(true)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/CoverHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.ContentValues\nimport android.graphics.Bitmap\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport android.view.GestureDetector\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewGroup\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport java.io.File\n\nclass CoverHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    private val gson by lazy {\n        instance.gson()?.let { instance.gsonConverterClass?.getStaticObjectField(it) }\n    }\n\n    override fun startHook() {\n//        if (!sPrefs.getBoolean(\"get_cover\", false)) return\n//        Log.d(\"startHook: GetCover\")\n//        arrayOf(bgmClass, ugcClass, liveClass).forEach {\n//            it?.hookAfterMethod(\n//                \"onViewCreated\",\n//                View::class.java,\n//                Bundle::class.java,\n//                hooker = hooker\n//            )\n//        }\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    val hooker: Hooker = { param ->\n        val group = param.args[0] as ViewGroup\n        val activity = param.thisObject.callMethodAs<Activity>(\"getActivity\")\n\n        val gestureDetector =\n            GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {\n                override fun onLongPress(e: MotionEvent) {\n                    var url: String? = null\n                    var filename: String? = null\n                    var title: String? = null\n                    try {\n                        when (param.thisObject.javaClass) {\n                            bgmClass -> activity.run {\n                                val viewModelField =\n                                    activity.javaClass.declaredFields.firstOrNull { it.type.name == \"com.bilibili.bangumi.logic.page.detail.BangumiDetailViewModelV2\" }\n                                val episodeMethod =\n                                    viewModelField?.type?.declaredMethods?.lastOrNull { it.returnType.name == \"com.bilibili.bangumi.data.page.detail.entity.BangumiUniformEpisode\" }\n                                val episode =\n                                    getObjectField(viewModelField?.name)?.callMethod(episodeMethod?.name)\n                                val hasGson = episode?.javaClass?.annotations?.any {\n                                    it.annotationClass.java.name.startsWith(\"gsonannotator\")\n                                } ?: false && instance.gsonFromJson() != null && instance.gsonToJson() != null\n                                if (hasGson) {\n                                    val json =\n                                        gson?.callMethodAs<String>(instance.gsonToJson(), episode)\n                                            ?.toJSONObject()\n                                    url = json?.optString(\"cover\")\n                                    filename = \"ep${json?.optInt(\"id\")}\"\n                                    title = json?.optString(\"share_copy\")\n                                } else {\n                                    url = episode?.getObjectFieldAs(\"cover\")\n                                    filename = \"ep${episode?.getLongField(\"epid\").toString()}\"\n                                    title = episode?.getObjectFieldAs(\"longTitle\") ?: \"\"\n                                }\n                            }\n                            ugcClass -> activity.run {\n                                javaClass.declaredFields.firstOrNull { it.type == instance.biliVideoDetailClass }\n                                    ?.let {\n                                        url = getObjectField(it.name)?.getObjectFieldAs(\"mCover\")\n                                        filename = \"av${\n                                            getObjectField(it.name)?.getObjectField(\"mAvid\")\n                                                .toString()\n                                        }\"\n                                        title = getObjectField(it.name)?.getObjectFieldAs(\"mTitle\")\n                                    }\n                            }\n                            else -> if (liveClass?.isInstance(param.thisObject) == true) {\n                                val viewModelField = activity.javaClass.declaredFields.firstOrNull {\n                                    it.type.name == \"com.bilibili.bililive.videoliveplayer.ui.roomv3.base.viewmodel.LiveRoomRootViewModel\" ||\n                                            it.type.name == \"com.bilibili.bililive.room.ui.roomv3.base.viewmodel.LiveRoomRootViewModel\"\n                                }\n                                val roomFeedField =\n                                    viewModelField?.type?.declaredFields?.firstOrNull {\n                                        it.type.name == \"com.bilibili.bililive.videoliveplayer.ui.roomv3.vertical.roomfeed.LiveRoomFeedViewModel\" ||\n                                                it.type.name == \"com.bilibili.bililive.room.ui.roomv3.vertical.roomfeed.LiveRoomFeedViewModel\"\n                                    }\n                                val currentFeedField =\n                                    roomFeedField?.type?.declaredFields?.firstOrNull {\n                                        it.type.name.startsWith(\"com.bilibili.bililive.videoliveplayer.ui.roomv3.vertical.roomfeed\") ||\n                                                it.type.name.startsWith(\"com.bilibili.bililive.room.ui.roomv3.vertical.roomfeed\")\n                                    }\n                                val roomIdField =\n                                    currentFeedField?.type?.declaredFields?.firstOrNull { it.type == Long::class.java }\n                                val coverField =\n                                    currentFeedField?.type?.declaredFields?.firstOrNull { it.type == String::class.java }\n                                val titleField =\n                                    currentFeedField?.type?.declaredFields?.lastOrNull { it.type == String::class.java }\n                                activity.getObjectField(viewModelField?.name)\n                                    ?.getObjectField(roomFeedField?.name)\n                                    ?.getObjectField(currentFeedField?.name)?.run {\n                                        url = getObjectFieldAs(coverField?.name)\n                                        filename =\n                                            \"live${getObjectField(roomIdField?.name).toString()}\"\n                                        title = getObjectFieldAs(titleField?.name)\n                                    }\n                            }\n                        }\n                    } catch (e: Throwable) {\n                        Log.e(e)\n                    }\n\n                    Log.toast(\"开始获取封面\", true)\n                    getBitmapFromURL(url) { bitmap ->\n                        bitmap?.let {\n                            val relativePath =\n                                \"${Environment.DIRECTORY_PICTURES}${File.separator}bilibili\"\n\n                            @Suppress(\"DEPRECATION\")\n                            val contentValues = ContentValues().apply {\n                                put(MediaStore.MediaColumns.DISPLAY_NAME, filename)\n                                put(MediaStore.MediaColumns.MIME_TYPE, \"image/png\")\n                                put(MediaStore.MediaColumns.TITLE, title)\n                                put(\n                                    MediaStore.MediaColumns.DATE_ADDED,\n                                    System.currentTimeMillis() / 1000\n                                )\n                                put(\n                                    MediaStore.MediaColumns.DATE_MODIFIED,\n                                    System.currentTimeMillis() / 1000\n                                )\n                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                                    put(\n                                        MediaStore.MediaColumns.DATE_TAKEN,\n                                        System.currentTimeMillis()\n                                    )\n                                    put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)\n                                } else {\n                                    val path = File(\n                                        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),\n                                        \"bilibili\"\n                                    )\n                                    path.mkdirs()\n                                    put(\n                                        MediaStore.MediaColumns.DATA,\n                                        File(path, \"$filename.png\").absolutePath\n                                    )\n                                }\n                            }\n                            val resolver = activity.contentResolver\n                            val uri = resolver.insert(\n                                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,\n                                contentValues\n                            )\n                            try {\n                                resolver.openOutputStream(uri!!)?.use { stream ->\n                                    it.compress(Bitmap.CompressFormat.PNG, 100, stream)\n                                    Log.toast(\n                                        \"保存封面成功到\\n$relativePath${File.separator}$filename.png\",\n                                        true\n                                    )\n                                }\n                            } catch (e: Throwable) {\n                                Log.e(e)\n                                Log.toast(\"保存封面失败，可能已经保存或未授予权限\", true)\n                            }\n                        } ?: run {\n                            Log.toast(\"获取封面失败\", true)\n                            return@getBitmapFromURL\n                        }\n                    }\n                }\n            })\n\n        val onTouchListener = View.OnTouchListener { _, event ->\n            gestureDetector.onTouchEvent(event)\n            false\n        }\n\n        for (i in 0 until group.childCount) {\n            val view = group.getChildAt(i)\n            if (view.javaClass.name.startsWith(\"tv.danmaku.biliplayerv2.widget.gesture\")\n                || view.javaClass.name.startsWith(\"tv.danmaku.biliplayerimpl.gesture\")\n            ) {\n                view.setOnTouchListener(onTouchListener)\n            }\n        }\n        val liveId = getId(\"controller_underlay\")\n        activity.findViewById<View>(liveId)?.setOnTouchListener(onTouchListener)\n    }\n\n    val bgmClass by Weak {\n        \"com.bilibili.bangumi.ui.page.detail.playerV2.BangumiPlayerFragmentV2\".findClass(\n            mClassLoader\n        )\n    }\n    val ugcClass by Weak {\n        \"tv.danmaku.bili.ui.video.playerv2.UgcPlayerFragment\".findClassOrNull(\n            mClassLoader\n        ) ?: \"tv.danmaku.bili.videopage.player.UgcPlayerFragment\".findClassOrNull(mClassLoader)\n    }\n    val liveClass by Weak {\n        \"com.bilibili.bililive.blps.core.business.player.container.AbsLivePlayerFragment\".findClass(\n            mClassLoader\n        )\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/CustomThemeHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.graphics.Color\nimport android.util.SparseArray\nimport android.view.View\nimport androidx.annotation.ColorInt\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.ColorChooseDialog\nimport me.iacn.biliroaming.Constant.CURRENT_COLOR_KEY\nimport me.iacn.biliroaming.Constant.CUSTOM_COLOR_KEY\nimport me.iacn.biliroaming.Constant.DEFAULT_CUSTOM_COLOR\nimport me.iacn.biliroaming.utils.*\n\n/**\n * Created by iAcn on 2019/7/14\n * Email i@iacn.me\n */\nclass CustomThemeHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"custom_theme\", false)) return\n        Log.d(\"startHook: CustomTheme\")\n\n        instance.themeNameClass?.getStaticObjectFieldAs<MutableMap<String, Int>>(instance.themeName())\n            ?.run {\n                put(\"custom1\", CUSTOM_THEME_ID1)\n                put(\"custom2\", CUSTOM_THEME_ID2)\n            }\n\n        val colorArray =\n            instance.themeHelperClass?.getStaticObjectFieldAs<SparseArray<IntArray>>(instance.colorArray())\n        val primaryColor = customColor\n        colorArray?.put(CUSTOM_THEME_ID1, generateColorArray(primaryColor))\n        colorArray?.put(CUSTOM_THEME_ID2, generateColorArray(primaryColor))\n\n        val allThemes =\n            instance.builtInThemesClass?.getStaticObjectFieldAs<MutableMap<Long, Any>>(instance.allThemesField())\n\n        instance.skinList()?.let {\n            \"tv.danmaku.bili.ui.theme.ThemeStoreActivity\".hookBeforeMethod(\n                mClassLoader, it,\n                \"tv.danmaku.bili.ui.theme.api.BiliSkinList\", Boolean::class.javaPrimitiveType\n            ) { param ->\n                val biliSkinList = param.args[0]\n\n                val mList = biliSkinList.getObjectFieldAs<MutableList<Any>>(\"mList\")\n                val biliSkin =\n                    \"tv.danmaku.bili.ui.theme.api.BiliSkin\".findClassOrNull(mClassLoader)?.new()\n                        ?: return@hookBeforeMethod\n                biliSkin.setIntField(\n                    \"mId\",\n                    if (currentKey == CUSTOM_THEME_ID2) CUSTOM_THEME_ID2 else CUSTOM_THEME_ID1\n                )\n                    .setObjectField(\"mName\", \"自选颜色\")\n                    .setBooleanField(\"mIsFree\", true)\n                // Under the night mode item\n                mList.add(3, biliSkin)\n                Log.d(\"Add a theme item: size = \" + mList.size)\n            }\n        }\n        instance.themeListClickClass?.hookBeforeMethod(\"onClick\", View::class.java) { param ->\n            val view = param.args[0] as View\n            val idName = view.resources.getResourceEntryName(view.id)\n            if (\"list_item\" != idName) return@hookBeforeMethod\n            val biliSkin = view.tag ?: return@hookBeforeMethod\n            val mId = biliSkin.getIntField(\"mId\")\n            // Make colors updated immediately\n            if (mId == CUSTOM_THEME_ID1 || mId == CUSTOM_THEME_ID2) {\n                view.context.addModuleAssets()\n                val colorDialog = ColorChooseDialog(view.context, customColor)\n                colorDialog.setPositiveButton(\"确定\") { _, _ ->\n                    val color = colorDialog.color\n                    val colors = generateColorArray(color)\n                    colorArray?.put(CUSTOM_THEME_ID1, colors)\n                    colorArray?.put(CUSTOM_THEME_ID2, colors)\n                    color.toTheme()?.let {\n                        allThemes?.put(CUSTOM_THEME_ID1.toLong(), it)\n                        allThemes?.put(CUSTOM_THEME_ID2.toLong(), it)\n                    }\n                    val newId = if (mId == CUSTOM_THEME_ID1) CUSTOM_THEME_ID2 else CUSTOM_THEME_ID1\n                    biliSkin.setIntField(\"mId\", newId)\n                    customColor = color\n                    try {\n                        param.invokeOriginalMethod()\n                    } catch (e: Exception) {\n                        Log.w(e)\n                    }\n                }\n                colorDialog.show()\n\n                // Stop executing the original method\n                param.result = null\n            }\n        }\n\n        // No reset when not logged in\n        val replacer: Replacer = { param ->\n            if (Thread.currentThread().stackTrace.any { s ->\n                    s.className == \"tv.danmaku.bili.MainActivityV2\" && s.methodName == \"onPostCreate\"\n                }) null else param.invokeOriginalMethod()\n        }\n        instance.themeReset().forEach {\n            instance.themeProcessorClass?.replaceMethod(it, replacer = replacer)\n        }\n    }\n\n    override fun lateInitHook() {\n        if (!sPrefs.getBoolean(\"custom_theme\", false)) return\n\n        val primaryColor = customColor\n\n        val allThemes =\n            instance.builtInThemesClass?.getStaticObjectFieldAs<MutableMap<Long, Any>>(instance.allThemesField())\n        primaryColor.toTheme()?.let {\n            allThemes?.put(CUSTOM_THEME_ID1.toLong(), it)\n            allThemes?.put(CUSTOM_THEME_ID2.toLong(), it)\n        }\n    }\n\n    fun insertColorForWebProcess() {\n        if (!sPrefs.getBoolean(\"custom_theme\", false)) return\n\n        try {\n            var cacheColor = customColor\n            var generatedColorArray = generateColorArray(cacheColor)\n            instance.themeNameClass?.getStaticObjectFieldAs<MutableMap<String, Int>>(instance.themeName())\n                ?.run {\n                    put(\"custom1\", CUSTOM_THEME_ID1)\n                    put(\"custom2\", CUSTOM_THEME_ID2)\n                }\n            instance.columnHelperClass?.getStaticObjectFieldAs<SparseArray<IntArray>>(instance.columnColorArray())\n                ?.run {\n                    put(CUSTOM_THEME_ID1, generatedColorArray)\n                    put(CUSTOM_THEME_ID2, generatedColorArray)\n                }\n            instance.themeHelperClass?.getStaticObjectFieldAs<SparseArray<IntArray>>(instance.colorArray())\n                ?.run {\n                    put(CUSTOM_THEME_ID1, generatedColorArray)\n                    put(CUSTOM_THEME_ID2, generatedColorArray)\n                }\n            instance.themeIdHelperClass?.getStaticObjectFieldAs<SparseArray<Int>>(instance.colorId())\n                ?.run {\n                    put(CUSTOM_THEME_ID1, CUSTOM_THEME_ID1)\n                    put(CUSTOM_THEME_ID2, CUSTOM_THEME_ID2)\n                }\n\n            SparseArray::class.java.hookAfterMethod(\n                \"get\",\n                Int::class.javaPrimitiveType,\n                Object::class.java\n            ) { param ->\n                if (param.args[0] != CUSTOM_THEME_ID1 || param.args[0] != CUSTOM_THEME_ID2) return@hookAfterMethod\n                if (param.result?.javaClass == generatedColorArray.javaClass && param.result == generatedColorArray) {\n                    val newColor = customColor\n                    if (newColor != cacheColor) {\n                        generatedColorArray = generateColorArray(newColor)\n                        cacheColor = newColor\n                        @Suppress(\"UNCHECKED_CAST\")\n                        (param.thisObject as SparseArray<IntArray>).run {\n                            put(CUSTOM_THEME_ID1, generatedColorArray)\n                            put(CUSTOM_THEME_ID2, generatedColorArray)\n                        }\n                        param.result = generatedColorArray\n                    }\n                }\n            }\n        } catch (e: Throwable) {\n            Log.e(e)\n        }\n    }\n\n    companion object {\n        private const val CUSTOM_THEME_ID1 = 114514 // ん？\n        private const val CUSTOM_THEME_ID2 = 1919810 // ん？\n\n        private var customColor: Int\n            get() = biliPrefs.getInt(CUSTOM_COLOR_KEY, DEFAULT_CUSTOM_COLOR)\n            set(value) = biliPrefs.edit().putInt(CUSTOM_COLOR_KEY, value).apply()\n\n        private val currentKey: Int\n            get() = blkvPrefs.getInt(CURRENT_COLOR_KEY, 0)\n\n        /**\n         * Color Array\n         *\n         *\n         * index0: color primary        e.g. global main color.\n         * index1: color primary dark   e.g. tint when button be pressed.\n         * index2: color primary light  e.g. temporarily not used.\n         * index3: color primary trans  e.g. mini-tv cover on drawer.\n         */\n        @JvmStatic\n        private fun generateColorArray(primaryColor: Int): IntArray {\n            val colors = IntArray(4)\n            val hsv = FloatArray(3)\n            val result = FloatArray(3)\n            Color.colorToHSV(primaryColor, hsv)\n            colors[0] = primaryColor\n\n            // Decrease brightness\n            hsv.copyInto(result)\n            result[2] -= result[2] * 0.2f\n            colors[1] = Color.HSVToColor(result)\n\n            // Increase brightness\n            hsv.copyInto(result)\n            result[2] += result[2] * 0.1f\n            colors[2] = Color.HSVToColor(result)\n\n            // Increase transparency\n            colors[3] = -0x4c000000 or 0xFFFFFF and colors[1]\n            return colors\n        }\n\n        private fun @receiver:ColorInt Int.pack() = toLong() and 0xFFFFFFFF shl 32\n        private fun @receiver:ColorInt Int.toTheme() = instance.themeColorsConstructor?.run {\n            parameterTypes.let {\n                val garb = it[0].let { cls ->\n                    try {\n                        cls.new()\n                    } catch (err: NoSuchMethodError) {\n                        // GarbInfo in play v3.20.0\n                        cls.new(\n                            0L,     // id\n                            true,   // showDarkContent\n                            true,   // isPure\n                            \"\",     // homePrimaryBgPath\n                            0L,     // primary\n                            0L,     // secondary\n                            0L,     // background\n                            0L,     // textTitle\n                            0L      // actionIcon\n                        )\n                    }\n                }\n                newInstance(\n                    garb,                   // garb\n                    it[1].enumConstants[0], // currentDayNight ThemeDayNight#Day\n                    pack(),                 // primary !!important\n                    pack(),                 // secondary !!important\n                    Color.WHITE.pack(),     // background !!important\n                    Color.WHITE.pack(),     // textTitle !!important\n                    Color.WHITE.pack(),     // textSubtitle\n                    Color.WHITE.pack(),     // textOther\n                    Color.WHITE.pack(),     // actionIcon !!important\n                    true,                   // isPure\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/DanmakuHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass DanmakuHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    override fun startHook() {\n        val blockWeight = sPrefs.getInt(\"danmaku_filter_weight\", 0)\n        val disableVipDmColorful = sPrefs.getBoolean(\"disable_vip_dm_colorful\", false)\n        if (blockWeight <= 0 && !disableVipDmColorful) return\n        instance.dmMossClass?.hookBeforeMethod(\n            \"dmSegMobile\",\n            \"com.bapis.bilibili.community.service.dm.v1.DmSegMobileReq\",\n            instance.mossResponseHandlerClass\n        ) { param ->\n            param.args[1] = param.args[1].mossResponseHandlerProxy {\n                if (blockWeight > 0) filterDanmaku(it, blockWeight)\n                if (disableVipDmColorful) clearVipColorfulSrc(it)\n            }\n        }\n    }\n\n    private fun filterDanmaku(reply: Any?, blockWeight: Int) {\n        reply?.callMethodAs<List<Any>>(\"getElemsList\")?.filter {\n            it.callMethodAs<Int>(\"getWeight\") >= blockWeight\n        }?.let {\n            reply.callMethod(\"clearElems\")\n            reply.callMethod(\"addAllElems\", it)\n        }\n    }\n\n    private fun clearVipColorfulSrc(reply: Any?) {\n        reply?.callMethodOrNullAs<List<Any>>(\"getColorfulSrcList\")?.filter {\n            // DmColorfulType 60001 VipGradualColor\n            it.callMethodAs<Int>(\"getTypeValue\") != 60001\n        }?.let {\n            reply.callMethod(\"clearColorfulSrc\")\n            reply.callMethod(\"addAllColorfulSrc\", it)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/DialogBlurBackgroundHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Dialog\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.blurBackground\nimport me.iacn.biliroaming.utils.hookAfterMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass DialogBlurBackgroundHook(mClassLoader: ClassLoader) : BaseHook(mClassLoader) {\n    override fun startHook() {\n        if (sPrefs.getBoolean(\"dialog_blur_background\", false).not()) return\n        Log.d(\"startHook: DialogBlurBackgroundHook\")\n        Dialog::class.java.hookAfterMethod(\"onStart\") {\n            (it.thisObject as Dialog).window?.blurBackground()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/DownloadThreadHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.AlertDialog\nimport android.content.Context\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.NumberPicker\nimport android.widget.TextView\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass DownloadThreadHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"custom_download_thread\", false)) return\n        Log.d(\"startHook: DownloadThread\")\n        instance.downloadThreadListenerClass?.run {\n            hookBeforeAllConstructors { param ->\n                val view = param.args.find { it is TextView } as? TextView\n                    ?: return@hookBeforeAllConstructors\n                val visibility = if (view.tag as Int == 1) {\n                    view.text = \"自定义\"\n                    View.VISIBLE\n                } else {\n                    View.INVISIBLE\n                }\n                (view.parent as ViewGroup).getChildAt(1).visibility = visibility\n            }\n            replaceMethod(\"onClick\", View::class.java) { param ->\n                var textViewField: String? = null\n                var viewHostField: String? = null\n                declaredFields.forEach {\n                    when (it.type) {\n                        instance.downloadThreadViewHostClass -> viewHostField = it.name\n                        TextView::class.java -> textViewField = it.name\n                    }\n                }\n                val view = param.thisObject.getObjectFieldAs<TextView>(textViewField)\n                if (view.tag as? Int == 1) {\n                    AlertDialog.Builder(view.context).create().run {\n                        setTitle(\"自定义同时缓存数\")\n                        val numberPicker = NumberPicker(context).apply {\n                            minValue = 1\n                            maxValue = 64\n                            wrapSelectorWheel = false\n                            value = param.thisObject.getObjectField(viewHostField)\n                                ?.getIntField(instance.downloadingThread())\n                                ?: 1\n                        }\n                        setView(numberPicker, 50, 0, 50, 0)\n                        setButton(AlertDialog.BUTTON_POSITIVE, \"OK\") { _, _ ->\n                            view.tag = numberPicker.value\n                            param.invokeOriginalMethod()\n                        }\n                        show()\n                    }\n                } else {\n                    param.invokeOriginalMethod()\n                }\n            }\n        }\n        instance.reportDownloadThreadClass?.replaceMethod(\n            instance.reportDownloadThread(),\n            Context::class.java,\n            Int::class.javaPrimitiveType\n        ) {}\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/DrawerHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass DrawerHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    private var drawerLayout: Any? = null\n    private var navView: View? = null\n\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"drawer\", false)) return\n\n        Log.d(\"startHook: DrawerHook\")\n\n        runCatching {\n            instance.kanbanCallbackClass?.new(null)?.callMethod(instance.kanbanCallback(), null)\n        }\n\n        instance.mainActivityClass?.hookAfterMethod(\"onCreate\", Bundle::class.java) { param ->\n            val self = param.thisObject as Activity\n            val view = self.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)\n            (view.parent as ViewGroup).removeViewInLayout(view)\n            drawerLayout = instance.drawerLayoutClass?.new(self)\n            drawerLayout?.callMethod(\"addView\", view, 0, view.layoutParams)\n\n            val homeFragment = instance.homeUserCenterClass?.new()\n            val fragmentManager = self.callMethod(\"getSupportFragmentManager\")\n            fragmentManager?.callMethod(\"beginTransaction\")?.callMethod(\"add\", homeFragment, \"home\")\n                ?.callMethod(\"commit\")\n            fragmentManager?.callMethod(\"executePendingTransactions\")\n\n            self.setContentView(drawerLayout as View)\n        }\n\n        val createHooker: Hooker = { param ->\n            val self = param.thisObject as Activity\n            val fragmentManager = self.callMethod(\"getSupportFragmentManager\")\n            navView = fragmentManager?.callMethod(\"findFragmentByTag\", \"home\")\n                ?.callMethodAs<View>(\"getView\")\n\n            val layoutParams = instance.drawerLayoutParamsClass?.new(\n                ViewGroup.MarginLayoutParams(\n                    ViewGroup.MarginLayoutParams.MATCH_PARENT,\n                    ViewGroup.MarginLayoutParams.MATCH_PARENT\n                )\n            )\n            layoutParams?.javaClass?.fields?.get(0)?.set(layoutParams, Gravity.START)\n            navView?.parent ?: drawerLayout?.callMethod(\"addView\", navView, 1, layoutParams)\n        }\n\n        instance.mainActivityClass?.runCatching {\n            getDeclaredMethod(\n                \"onPostCreate\",\n                Bundle::class.java\n            )\n        }?.onSuccess { it.hookAfterMethod(createHooker) }\n\n        instance.mainActivityClass?.runCatching { getDeclaredMethod(\"onStart\") }\n            ?.onSuccess { it.hookAfterMethod(createHooker) }\n\n        instance.mainActivityClass?.replaceMethod(\"onBackPressed\") { param ->\n            try {\n                if (drawerLayout?.callMethodAs<Boolean>(instance.isDrawerOpen(), navView) == true) {\n                    drawerLayout?.callMethod(instance.closeDrawer(), navView, true)\n                } else {\n                    param.invokeOriginalMethod()\n                }\n            } catch (e: Throwable) {\n                param.invokeOriginalMethod()\n            }\n        }\n\n        \"tv.danmaku.bili.ui.main2.basic.BaseMainFrameFragment\".hookAfterMethod(\n            mClassLoader,\n            \"onViewCreated\",\n            View::class.java,\n            Bundle::class.java\n        ) { param ->\n            val id = getId(\"avatar_layout\")\n            (param.args[0] as View).findViewById<View>(id)?.setOnClickListener {\n                try {\n                    drawerLayout?.callMethod(instance.openDrawer(), navView, true)\n                } catch (e: Throwable) {\n                    Log.e(e)\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/DynamicHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass DynamicHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    private val dynamicMossV1 by Weak { \"com.bapis.bilibili.app.dynamic.v1.DynamicMoss\" from mClassLoader }\n    private val dynamicMossV2 by Weak { \"com.bapis.bilibili.app.dynamic.v2.DynamicMoss\" from mClassLoader }\n\n    private val purifyTypes = run {\n        sPrefs.getStringSet(\"customize_dynamic_type\", null)\n            ?.map { it.toInt() }.orEmpty()\n    }\n    private val purifyContents = run {\n        sPrefs.getStringSet(\"customize_dynamic_keyword_content\", null).orEmpty()\n    }\n    private val purifyContentRegexes by lazy { purifyContents.map { it.toRegex() } }\n    private val contentRegexMode = sPrefs.getBoolean(\"dynamic_content_regex_mode\", false)\n    private val purifyUpNames = run {\n        sPrefs.getStringSet(\"customize_dynamic_keyword_upname\", null).orEmpty()\n    }\n    private val purifyUidList = run {\n        sPrefs.getStringSet(\"customize_dynamic_keyword_uid\", null)\n            ?.mapNotNull { it.toLongOrNull() }.orEmpty()\n    }\n    private val removeTopicOfAll = sPrefs.getBoolean(\"customize_dynamic_all_rm_topic\", false)\n    private val removeUpOfAll = sPrefs.getBoolean(\"customize_dynamic_all_rm_up\", false)\n    private val removeLiveOfAll = sPrefs.getBoolean(\"customize_dynamic_all_rm_live\", false)\n    private val removeUpOfVideo = sPrefs.getBoolean(\"customize_dynamic_video_rm_up\", false)\n    private val preferVideoTab = sPrefs.getBoolean(\"prefer_video_tab\", false)\n    private val filterApplyToVideo = sPrefs.getBoolean(\"filter_apply_to_video\", false)\n    private val rmBlocked = sPrefs.getBoolean(\"customize_dynamic_rm_blocked\", false)\n    private val rmAdLink = sPrefs.getBoolean(\"customize_dynamic_rm_ad_link\", false)\n\n    private val needFilterDynamic = purifyTypes.isNotEmpty() || purifyContents.isNotEmpty()\n            || purifyUpNames.isNotEmpty() || purifyUidList.isNotEmpty() || rmBlocked\n\n    override fun startHook() {\n        val hidden = sPrefs.getBoolean(\"hidden\", false)\n        if (hidden && (needFilterDynamic || removeTopicOfAll || removeUpOfAll || removeLiveOfAll)) {\n            dynamicMossV2?.hookBeforeMethod(\n                \"dynAll\",\n                \"com.bapis.bilibili.app.dynamic.v2.DynAllReq\",\n                instance.mossResponseHandlerClass\n            ) { param ->\n                param.args[1] = param.args[1].mossResponseHandlerProxy { reply ->\n                    reply ?: return@mossResponseHandlerProxy\n                    Log.d(\"upList: ${reply.callMethod(\"getUpList\")}\")\n                    if (removeTopicOfAll)\n                        reply.callMethod(\"clearTopicList\")\n                    if (removeUpOfAll || removeLiveOfAll) {\n                        reply.callMethod(\"getUpList\")?.runCatching UpList@{\n                            val upList = this@UpList\n                            val firstList =\n                                upList.callMethodAs(\"getListList\") ?: emptyList<Any>()\n                            val secondList =\n                                upList.callMethodAs(\"getListSecondList\") ?: emptyList<Any>()\n                            val allUpItems = firstList + secondList\n                            val newItems = allUpItems.filter { item ->\n                                val liveState = item.callMethodAs<Int>(\"getLiveStateValue\")\n                                val isLive = liveState > 0\n                                when {\n                                    removeUpOfAll && removeLiveOfAll -> false\n                                    removeUpOfAll -> isLive\n                                    else -> !isLive\n                                }\n                            }.onEachIndexed { index, item ->\n                                item.callMethod(\"setPos\", index + 1L)\n                            }\n                            upList.callMethod(\"clearList\")\n                            upList.callMethod(\"clearListSecond\")\n                            upList.callMethod(\"addAllList\", newItems)\n                        }?.onFailure { Log.e(it) }\n                    }\n                    if (needFilterDynamic)\n                        filterDynamic(reply)\n                }\n            }\n        }\n        if (hidden && ((filterApplyToVideo && needFilterDynamic) || removeUpOfVideo)) {\n            dynamicMossV2?.hookBeforeMethod(\n                \"dynVideo\",\n                \"com.bapis.bilibili.app.dynamic.v2.DynVideoReq\",\n                instance.mossResponseHandlerClass\n            ) {\n                it.args[1] = it.args[1].mossResponseHandlerProxy { result ->\n                    result?.let {\n                        if (removeUpOfVideo)\n                            it.callMethod(\"clearVideoUpList\")\n                        if (filterApplyToVideo && needFilterDynamic)\n                            filterDynamic(it)\n                    }\n                }\n            }\n        }\n        if (hidden && preferVideoTab) {\n            dynamicMossV2?.hookBeforeMethod(\n                \"dynTab\",\n                \"com.bapis.bilibili.app.dynamic.v2.DynTabReq\",\n                instance.mossResponseHandlerClass\n            ) {\n                it.args[1] = it.args[1].mossResponseHandlerReplaceProxy { reply ->\n                    reply?.callMethod(\"ensureScreenTabIsMutable\")\n                    reply?.callMethodAs<MutableList<Any>>(\"getScreenTabList\")?.map {\n                        it.callMethod(\n                            \"setDefaultTab\",\n                            it.callMethodAs<String>(\"getName\") == \"video\"\n                        )\n                    }\n                    reply\n                }\n            }\n            dynamicMossV1?.hookAfterMethod(\n                if (instance.useNewMossFunc) \"executeDynRed\" else \"dynRed\",\n                \"com.bapis.bilibili.app.dynamic.v1.DynRedReq\"\n            ) { param ->\n                param.result?.callMethod(\"setDefaultTab\", \"video\")\n            }\n        }\n    }\n\n    private fun filterDynamic(reply: Any) {\n        val dynamicList = reply.callMethod(\"getDynamicList\") ?: return\n        dynamicList.callMethod(\"ensureListIsMutable\")\n        val contentList = dynamicList.callMethodAs<MutableList<Any>>(\"getListList\")\n        val idxList = mutableSetOf<Int>()\n        for ((idx, e) in contentList.withIndex()) {\n            if (purifyTypes.isNotEmpty()) {\n                val cardType = e.callMethodAs<Int>(\"getCardTypeValue\")\n                if (purifyTypes.contains(cardType)) {\n                    idxList.add(idx)\n                    continue\n                }\n            }\n\n            val extend = e.callMethod(\"getExtend\") ?: continue\n            if (rmBlocked\n                && extend.callMethodOrNull(\"hasOnlyFansProperty\") == true\n                && extend.callMethod(\"getOnlyFansProperty\")?.callMethod(\"getHasPrivilege\") == false\n            ) {\n                idxList.add(idx)\n                continue\n            }\n\n            if (rmAdLink || purifyContents.isNotEmpty()) {\n                val moduleList = e.callMethodAs<List<Any>>(\"getModulesList\")\n                if (rmAdLink && moduleList.any {\n                        it.callMethodOrNull(\"hasModuleAdditional\") == true\n                                && it.callMethod(\"getModuleAdditional\")\n                            ?.callMethod(\"getTypeValue\").let {\n                                it == 2 || it == 6\n                            } // additional_type_goods, additional_type_up_rcmd\n                    }) {\n                    idxList.add(idx)\n                    continue\n                }\n                val modulesText = moduleList.joinToString(separator = \"\") {\n                    it.callMethod(\"getModuleDesc\")\n                        ?.callMethodAs<String>(\"getText\").orEmpty()\n                }\n                if (modulesText.isNotEmpty() && if (contentRegexMode)\n                        purifyContentRegexes.any { modulesText.contains(it) }\n                    else purifyContents.any { modulesText.contains(it) }\n                ) {\n                    idxList.add(idx)\n                    continue\n                }\n            }\n\n            if (purifyContents.isNotEmpty()) {\n                val contentOrig = extend\n                    .callMethodAs<List<Any>>(\"getOrigDescList\")\n                    .joinToString(separator = \"\") {\n                        it.callMethodAs<String>(\"getOrigText\")\n                    }\n                if (contentOrig.isNotEmpty() && if (contentRegexMode)\n                        purifyContentRegexes.any { contentOrig.contains(it) }\n                    else purifyContents.any { contentOrig.contains(it) }\n                ) {\n                    idxList.add(idx)\n                    continue\n                }\n            }\n\n            if (purifyContents.isNotEmpty()) {\n                val content = extend\n                    .callMethodAs<List<Any>>(\"getDescList\")\n                    .joinToString(separator = \"\") {\n                        it.callMethodAs<String>(\"getOrigText\")\n                    }\n                if (content.isNotEmpty() && if (contentRegexMode)\n                        purifyContentRegexes.any { content.contains(it) }\n                    else purifyContents.any { content.contains(it) }\n                ) {\n                    idxList.add(idx)\n                    continue\n                }\n            }\n\n            if (purifyUpNames.isNotEmpty()) {\n                val origName = extend.callMethodAs<String>(\"getOrigName\")\n                if (origName.isNotEmpty() && purifyUpNames.any { origName == it }) {\n                    idxList.add(idx)\n                    continue\n                }\n            }\n\n            if (purifyUidList.isNotEmpty()) {\n                val uid = extend.callMethodAs<Long>(\"getUid\")\n                if (uid > 0L && purifyUidList.any { uid == it }) {\n                    idxList.add(idx)\n                    continue\n                }\n            }\n        }\n        idxList.reversed().forEach {\n            contentList.removeAt(it)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/EnvHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.content.SharedPreferences\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport java.lang.reflect.Proxy\nimport java.util.regex.Pattern\n\nclass EnvHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        Log.d(\"startHook: Env\")\n\n        // EnvContext\n        instance.preBuiltConfigClass?.let {\n            val hooker: Hooker = hooker@ { param ->\n                @Suppress(\"UNCHECKED_CAST\")\n                val result = param.result as MutableMap<String, String?>\n                for (config in configSet) {\n                    (if (sPrefs.getBoolean(\n                            config.config,\n                            false\n                        )\n                    ) config.trueValue else config.falseValue)\n                        ?.let { result[config.key] = it } ?: result.remove(config.key)\n                }\n            }\n            // v8.28.0 - ?\n            it.hookAfterMethod(instance.getPreBuiltConfigMethod(), hooker = hooker)\n            // ? - v8.48.0 ..\n            it.hookAfterMethod(instance.getPreBuiltConfigMethod(), it, hooker = hooker)\n        }\n\n        // TypedContext\n        instance.dataSPClass?.let {\n            val hooker: Hooker = hooker@ { param ->\n                val result = param.result as SharedPreferences\n                // this indicates the proper instance\n                if (!result.contains(\"bv.enable_bv\")) return@hooker\n                for (config in configSet) {\n                    (if (sPrefs.getBoolean(\n                            config.config,\n                            false\n                        )\n                    ) config.trueValue else config.falseValue)\n                        ?.let { result.edit().putString(config.key, it).apply() }\n                        ?: result.edit().remove(config.key).apply()\n                }\n            }\n            // v8.28.0 - ?\n            it.hookAfterMethod(instance.getDataSPMethod(), hooker = hooker)\n            // ? - v8.48.0 ..\n            it.hookAfterMethod(instance.getDataSPMethod(), it, hooker = hooker)\n        }\n\n        \"com.bilibili.lib.blconfig.internal.OverrideConfig\".findClassOrNull(mClassLoader)\n            ?.hookBeforeAllConstructors { param ->\n                val delegate = param.args.getOrNull(0) ?: return@hookBeforeAllConstructors\n                val realConfig = param.args.getOrNull(1) ?: return@hookBeforeAllConstructors\n                val delegateClass = delegate.javaClass\n                param.args[0] = Proxy.newProxyInstance(\n                    delegateClass.classLoader,\n                    delegateClass.interfaces\n                ) { _, m, a ->\n                    val args = a ?: emptyArray()\n                    if (m.name == \"getConfig\") {\n                        var result: Any? = null\n                        val key = args[0]\n                        for (config in configSet) {\n                            if (sPrefs.getBoolean(config.config, false) && config.key == key) {\n                                result = realConfig.callMethodOrNull(\"get\", *args)\n                            }\n                        }\n                        result ?: m(delegate, *args)\n                    } else {\n                        m(delegate, *args)\n                    }\n                }\n            }\n\n//        // Disable tinker\n//        \"com.tencent.tinker.loader.app.TinkerApplication\".findClass(mClassLoader)?.hookBeforeAllConstructors { param ->\n//            param.args[0] = 0\n//        }\n    }\n\n    override fun lateInitHook() {\n        Log.d(\"lateHook: Env\")\n        if (sPrefs.getBoolean(\"enable_av\", false)) {\n            val compatClass = \"com.bilibili.droid.BVCompat\".findClassOrNull(mClassLoader)\n            compatClass?.declaredFields?.forEach {\n                val field = compatClass.getStaticObjectField(it.name)\n                if (field is Pattern && field.pattern() == \"av[1-9]\\\\d*\")\n                    compatClass.setStaticObjectField(\n                        it.name,\n                        Pattern.compile(\"(av[1-9]\\\\d*)|(BV1[1-9A-NP-Za-km-z]{9})\", field.flags())\n                    )\n            }\n        }\n    }\n\n    companion object {\n\n        private val encryptedValueMap = hashMapOf(\n            \"0\" to \"Irb5O7Q8Ka0ojD4qqScgqg==\",\n            \"1\" to \"Y260Cyvp6HZEboaGO+YGMw==\"\n        )\n\n        class ConfigTuple(\n            val key: String,\n            val config: String,\n            val trueValue: String?,\n            val falseValue: String?\n        )\n\n        val configSet = listOf(\n            ConfigTuple(\n                \"bv.enable_bv\",\n                \"enable_av\",\n                encryptedValueMap[\"0\"],\n                encryptedValueMap[\"1\"]\n            ),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/FullStoryHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.replaceMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass FullStoryHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"disable_story_full\", false)) return\n        instance.playerFullStoryWidgets().forEach { (clazz, method) ->\n            clazz?.replaceMethod(method, clazz) { false }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/HintHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport android.os.Bundle\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.R\nimport me.iacn.biliroaming.utils.addModuleAssets\nimport me.iacn.biliroaming.utils.hookAfterMethod\nimport me.iacn.biliroaming.utils.inflateLayout\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass HintHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"show_hint\", true)) return\n        instance.mainActivityClass?.hookAfterMethod(\"onCreate\", Bundle::class.java) { param ->\n            AlertDialog.Builder(param.thisObject as Activity).run {\n                context.addModuleAssets()\n                setTitle(\"哔哩漫游使用说明\")\n                setView(context.inflateLayout(R.layout.feature))\n                setNegativeButton(\"知道了\") { _, _ ->\n                    sPrefs.edit().putBoolean(\"show_hint\", false).apply()\n                }\n                show()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/JsonHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport java.lang.reflect.Type\n\nclass JsonHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    companion object {\n        val bottomItems = mutableListOf<BottomItem>()\n        val drawerItems = mutableListOf<BottomItem>()\n    }\n\n    override fun startHook() {\n        Log.d(\"startHook: Json\")\n\n        val hidden = sPrefs.getBoolean(\"hidden\", false)\n        val purifyLivePopups = sPrefs.getStringSet(\"purify_live_popups\", null) ?: setOf()\n        val unlockPlayLimit = sPrefs.getBoolean(\"play_arc_conf\", false)\n\n        val tabResponseClass =\n            \"tv.danmaku.bili.ui.main2.resource.MainResourceManager\\$TabResponse\".findClassOrNull(\n                mClassLoader\n            )\n        val accountMineClass =\n            \"tv.danmaku.bili.ui.main2.api.AccountMine\".findClassOrNull(mClassLoader)\n        val garbEntranceClass =\n            \"tv.danmaku.bili.ui.main2.api.AccountMine\\$GarbEntrance\".from(mClassLoader)\n        val splashClass = \"tv.danmaku.bili.ui.splash.SplashData\".findClassOrNull(mClassLoader)\n            ?: \"tv.danmaku.bili.ui.splash.ad.model.SplashData\".findClassOrNull(mClassLoader)\n        val splashShowClass = \"tv.danmaku.bili.ui.splash.ad.model.SplashShowData\".findClassOrNull(mClassLoader)\n        val tabClass =\n            \"tv.danmaku.bili.ui.main2.resource.MainResourceManager\\$Tab\".findClassOrNull(\n                mClassLoader\n            )\n        val defaultWordClass =\n            \"tv.danmaku.bili.ui.main2.api.SearchDefaultWord\".findClassOrNull(mClassLoader)\n        val defaultKeywordClass =\n            \"com.bilibili.search.api.DefaultKeyword\".findClassOrNull(mClassLoader)\n        val brandSplashDataClass =\n            \"tv.danmaku.bili.ui.splash.brand.BrandSplashData\".findClassOrNull(mClassLoader)\n                ?: \"tv.danmaku.bili.ui.splash.brand.model.BrandSplashData\".findClassOrNull(mClassLoader)\n        val eventEntranceClass =\n            \"tv.danmaku.bili.ui.main.event.model.EventEntranceModel\".findClassOrNull(mClassLoader)\n        val searchRanksClass = \"com.bilibili.search.api.SearchRanks\".findClassOrNull(mClassLoader)\n        val searchReferralClass =\n            \"com.bilibili.search.api.SearchReferral\".findClassOrNull(mClassLoader)\n        val searchReferralV2Class =\n            \"com.bilibili.search2.api.SearchReferral\".findClassOrNull(mClassLoader)\n        val followingcardSearchRanksClass =\n            \"com.bilibili.bplus.followingcard.net.entity.b\".findClassOrNull(mClassLoader)\n        val spaceClass =\n            \"com.bilibili.app.authorspace.api.BiliSpace\".findClassOrNull(mClassLoader)\n        val ogvApiResponseClass =\n            \"tv.danmaku.bili.ui.offline.api.OgvApiResponse\".findClassOrNull(mClassLoader)\n        val ogvApiResponseV2Class =\n            \"tv.danmaku.bili.ui.offline.api.OgvApiResponseV2\".findClassOrNull(mClassLoader)\n        val dmAdvertClass =\n            \"com.bilibili.ad.adview.videodetail.danmakuv2.model.DmAdvert\".from(mClassLoader)\n        val liveShoppingInfoClass =\n            \"com.bilibili.bililive.room.biz.shopping.beans.LiveShoppingInfo\".from(mClassLoader)\n        val liveGoodsCardInfoClass =\n            \"com.bilibili.bililive.room.biz.shopping.beans.LiveGoodsCardInfo\".from(mClassLoader)\n        val liveRecommendCardGoodsClass =\n            \"com.bilibili.bililive.room.biz.shopping.beans.LiveShoppingRecommendCardGoodsDetail\"\n                .from(mClassLoader)\n        val biliLiveRoomInfoClass =\n            \"com.bilibili.bililive.videoliveplayer.net.beans.gateway.roominfo.BiliLiveRoomInfo\"\n                .from(mClassLoader)\n        val liveRoomReserveInfoClass =\n            \"com.bilibili.bililive.room.biz.reverse.bean.LiveRoomReserveInfo\".from(mClassLoader)\n        val biliLiveRoomUserInfoClass =\n            \"com.bilibili.bililive.videoliveplayer.net.beans.gateway.userinfo.BiliLiveRoomUserInfo\"\n                .from(mClassLoader)\n        val liveRoomRecommendCardClass =\n            \"com.bilibili.bililive.videoliveplayer.net.beans.attentioncard.LiveRoomRecommendCard\"\n                .from(mClassLoader)\n        val liveShoppingGotoBuyInfoClass =\n            \"com.bilibili.bililive.room.biz.shopping.beans.LiveShoppingGotoBuyInfo\"\n                .from(mClassLoader)\n        val shareChannelsClass = \"com.bilibili.lib.sharewrapper.online.api.ShareChannels\"\n            .from(mClassLoader)\n        val channelItemClass = \"com.bilibili.lib.sharewrapper.online.api.ShareChannels\\$ChannelItem\"\n            .from(mClassLoader)\n\n        instance.fastJsonClass?.hookAfterMethod(\n            instance.fastJsonParse(),\n            String::class.java,\n            Type::class.java,\n            Int::class.javaPrimitiveType,\n            \"com.alibaba.fastjson.parser.Feature[]\"\n        ) { param ->\n            var result = param.result ?: return@hookAfterMethod\n            if (result.javaClass == instance.generalResponseClass) {\n                result = result.getObjectField(\"data\") ?: return@hookAfterMethod\n            }\n\n            when (result.javaClass) {\n                tabResponseClass -> {\n                    val data = result.getObjectField(\"tabData\")\n\n                    bottomItems.clear()\n                    val hides = sPrefs.getStringSet(\"hided_bottom_items\", mutableSetOf())!!\n                    data?.getObjectFieldAs<MutableList<*>?>(\"bottom\")?.removeAll {\n                        val uri = it?.getObjectFieldAs<String>(\"uri\")\n                        val id = it?.getObjectFieldAs<String>(\"tabId\")\n                        val showing = id !in hides\n                        bottomItems.add(\n                            BottomItem(\n                                it?.getObjectFieldAs(\"name\"),\n                                uri, id, showing\n                            )\n                        )\n                        showing.not()\n                    }\n\n                    if (sPrefs.getBoolean(\"drawer\", false) && !sPrefs.getBoolean(\"hidden\", false)) {\n                        data?.getObjectFieldAs<MutableList<*>?>(\"bottom\")?.removeAll {\n                            it?.getObjectFieldAs<String?>(\"uri\")\n                                ?.startsWith(\"bilibili://user_center/mine\")\n                                ?: false\n                        }\n                    }\n\n                    configTab(data, tabClass)\n\n                    if (sPrefs.getBoolean(\"purify_game\", false) &&\n                        sPrefs.getBoolean(\"hidden\", false)\n                    ) {\n                        val top = data?.getObjectFieldAs<MutableList<*>?>(\"top\")\n                        top?.removeAll {\n                            val uri = it?.getObjectFieldAs<String?>(\"uri\")\n                            uri?.startsWith(\"bilibili://game_center/home\") ?: false\n                        }\n                    }\n\n                }\n                accountMineClass -> {\n                    drawerItems.clear()\n                    val hides = sPrefs.getStringSet(\"hided_drawer_items\", mutableSetOf())!!\n                    if (platform == \"android_hd\") {\n                        listOf(result.getObjectFieldOrNullAs<MutableList<*>?>(\"padSectionList\"),\n                               result.getObjectFieldOrNullAs<MutableList<*>?>(\"recommendSectionList\"),\n                               result.getObjectFieldOrNullAs<MutableList<*>?>(\"moreSectionList\")\n                        ).forEach {\n                            it?.removeAll { items ->\n                                // 分析内容\n                                val title = items?.getObjectFieldAs<String>(\"title\")\n                                val uri = items?.getObjectFieldAs<String>(\"uri\")\n                                val id = items?.getObjectField(\"id\").toString()\n\n                                // 修改成自定义按钮\n                                if (sPrefs.getBoolean(\"add_custom_button\", false) && id == sPrefs.getString(\"custom_button_id\", \"\")){\n                                    val icon = items?.getObjectFieldAs<String>(\"icon\").toString()\n                                    items?.setObjectField(\"title\", sPrefs.getString(\"custom_button_title\", title))\n                                        ?.setObjectField(\"uri\", sPrefs.getString(\"custom_button_uri\", uri))\n                                        ?.setObjectField(\"icon\", sPrefs.getString(\"custom_button_icon\", icon))\n                                    return@removeAll false\n                                }\n\n                                val showing = id !in hides\n                                // 将结果写入 drawerItems\n                                drawerItems.add(BottomItem(title, uri, id, showing))\n                                // 去除红点\n                                if (sPrefs.getBoolean(\"purify_drawer_reddot\", false)) items?.setIntField(\"redDot\",0)\n                                showing.not()\n                            }\n                        }\n                    } else {\n                        result.getObjectFieldOrNullAs<MutableList<*>?>(\"sectionListV2\")?.forEach { sections ->\n                            try {\n                                // 将标题写入 drawerItems\n                                val bigTitle = sections?.getObjectFieldOrNull(\"title\").toString()\n                                if (bigTitle != \"null\") drawerItems.add(BottomItem(\"【标题项目】\", null, bigTitle, bigTitle !in hides))\n                                // 去除项目\n                                sections?.getObjectFieldOrNullAs<MutableList<*>?>(\"itemList\")\n                                    ?.removeAll { items ->\n                                        // 分析内容\n                                        val title = try {\n                                            items?.getObjectFieldAs<String>(\"title\")\n                                        } catch (thr: Throwable) {\n                                            return@removeAll false\n                                        }\n                                        if (title == \"null\") return@removeAll false\n                                        val uri = items?.getObjectFieldAs<String>(\"uri\")\n                                        val id = items?.getObjectField(\"id\").toString()\n\n                                        // 修改成自定义按钮\n                                        if (sPrefs.getBoolean(\"add_custom_button\", false) && id == sPrefs.getString(\"custom_button_id\", \"\")){\n                                            val icon = items?.getObjectFieldAs<String>(\"icon\").toString()\n                                            items?.setObjectField(\"title\", sPrefs.getString(\"custom_button_title\", title))\n                                                ?.setObjectField(\"uri\", sPrefs.getString(\"custom_button_uri\", uri))\n                                                ?.setObjectField(\"icon\", sPrefs.getString(\"custom_button_icon\", icon))\n                                            return@removeAll false\n                                        }\n\n                                        val showing = id !in hides\n                                        // 将结果写入 drawerItems\n                                        drawerItems.add(BottomItem(title, uri, id, showing))\n                                        // 去除红点\n                                        if (sPrefs.getBoolean(\"purify_drawer_reddot\", false)) items?.setIntField(\"redDot\", 0)\n                                        showing.not()\n                                    }\n                                // 去除按钮\n                                val button = sections?.getObjectFieldOrNull(\"button\")\n                                if (button != null) {\n                                    val buttonText = button.getObjectField(\"text\").toString()\n                                    val showing = buttonText !in hides\n                                    if (buttonText != \"null\") {\n                                        val uri = button.getObjectFieldAs<String>(\"jumpUrl\")\n                                        drawerItems.add(BottomItem(\"按钮：\", uri, buttonText, showing))\n                                        if (!showing) sections.setObjectField(\"button\", null)\n                                    }\n                                }\n                                // 改变样式\n                                if (sPrefs.getBoolean(\"drawer_style_switch\", false)) {\n                                    sections?.setIntField(\n                                        \"style\",\n                                        when {\n                                            sPrefs.getBoolean(\"drawer_style\", false) -> 2\n                                            else -> 1\n                                        }\n                                    )\n                                }\n                            } catch (e: Exception) {\n                                Log.d(e)\n                            }\n                        }\n                        // 删除标题组\n                        var deleteTitle = true\n                        result.getObjectFieldOrNullAs<MutableList<*>?>(\"sectionListV2\")?.removeAll { sections ->\n                            val title = sections?.getObjectFieldOrNull(\"title\").toString()\n                            if (title !in hides) {\n                                if (title == \"创作中心\" || title == \"游戏中心\" || title == \"創作中心\" || title == \"遊戲中心\")\n                                    deleteTitle = false // 标记\n                                else if (title == \"推荐服务\" || title == \"推薦服務\")\n                                    deleteTitle = true // 取消标记\n                            } else {\n                                if ((title == \"更多服务\" || title == \"更多服務\") && !deleteTitle) {\n                                    Log.toast(\"自定义我的页面，【标题项目】不能只保留【创作中心】或【游戏中心】，因此不删除【更多服务】，请修改你的漫游设置\", true)\n                                    return@removeAll false\n                                }\n                            }\n                            title in hides\n                        }\n\n                        // liveTip\n                        // 就是通常在 我的 -> 创做中心 下 但不属于创作中心的提示\n                        result.getObjectFieldOrNull(\"liveTip\")?.let { liveTip ->\n                            val text = liveTip.getObjectFieldAs<String>(\"text\")\n                            val id = liveTip.getObjectField(\"id\").toString()\n                            val url = liveTip.getObjectFieldAs<String>(\"url\")\n                            val showing = id !in hides\n                            drawerItems.add(BottomItem(\n                                name = text, id = id, uri = url, showing = showing\n                            ))\n                            if (!showing) {\n                                result.setObjectField(\"liveTip\", null)\n                            }\n                        }\n\n                    }\n                    accountMineClass.findFieldOrNull(\"vipSectionRight\")?.set(result, null)\n                    if (sPrefs.getBoolean(\"custom_theme\", false)) {\n                        if (instance.clientVersionCode >= 7480200) {\n                            garbEntranceClass?.new()?.apply {\n                                setObjectField(\"uri\", \"activity://navigation/theme/\")\n                            }?.let { result.setObjectField(\"garbEntrance\", it) }\n                        } else {\n                            result.setObjectField(\"garbEntrance\", null)\n                        }\n                    }\n                }\n                splashClass, splashShowClass -> if (sPrefs.getBoolean(\"purify_splash\", false) &&\n                    sPrefs.getBoolean(\"hidden\", false)\n                ) {\n                    result.getObjectFieldOrNullAs<MutableList<*>>(\"splashList\")?.clear()\n                    result.getObjectFieldOrNullAs<MutableList<*>>(\"strategyList\")?.clear()\n                }\n                defaultWordClass, defaultKeywordClass, searchRanksClass, searchReferralClass, followingcardSearchRanksClass -> if (sPrefs.getBoolean(\n                        \"purify_search\",\n                        false\n                    ) &&\n                    sPrefs.getBoolean(\"hidden\", false)\n                ) {\n                    result.javaClass.fields.forEach {\n                        if (!it.type.isPrimitive)\n                            result.setObjectField(it.name, null)\n                    }\n                }\n                searchReferralV2Class -> if (sPrefs.getBoolean(\n                        \"purify_search\",\n                        false\n                    ) &&\n                    sPrefs.getBoolean(\"hidden\", false)\n                ) {\n                    result.javaClass.declaredFields.forEach {\n                        it.isAccessible = true\n                        it.set(result, it.type.let { type ->\n                            when (type) {\n                                Int::class.javaPrimitiveType -> 0\n                                else -> null\n                            }\n                        })\n                    }\n                }\n                brandSplashDataClass -> if (sPrefs.getBoolean(\"custom_splash\", false) ||\n                    sPrefs.getBoolean(\"custom_splash_logo\", false)\n                ) {\n                    result.getObjectFieldOrNullAs<MutableList<Any>>(\"brandList\")?.clear()\n                    result.getObjectFieldOrNullAs<MutableList<Any>>(\"showList\")?.clear()\n                }\n                eventEntranceClass -> if (sPrefs.getBoolean(\"purify_game\", false) &&\n                    sPrefs.getBoolean(\"hidden\", false)\n                ) {\n                    result.setObjectField(\"online\", null)\n                    result.setObjectField(\"hash\", \"\")\n                }\n                spaceClass -> {\n                    val purifySpaceSet =\n                        sPrefs.getStringSet(\"customize_space\", emptySet()).orEmpty()\n                    if (purifySpaceSet.isNotEmpty()) {\n                        purifySpaceSet.forEach {\n                            if (!it.contains(\".\")) result.setObjectField(\n                                it,\n                                null\n                            )\n                        }\n                        // Exceptions (adV2 -> ad + adV2)\n                        if (purifySpaceSet.contains(\"adV2\")) result.setObjectField(\"ad\", null)\n\n                        result.getObjectFieldAs<MutableList<*>?>(\"tab\")?.removeAll {\n                            when (it?.getObjectFieldAs<String?>(\"param\")) {\n                                \"home\" -> purifySpaceSet.contains(\"tab.home\")\n                                \"dynamic\" -> purifySpaceSet.contains(\"tab.dynamic\")\n                                \"contribute\" -> purifySpaceSet.contains(\"tab.contribute\")\n                                \"shop\" -> purifySpaceSet.contains(\"tab.shop\")\n                                \"bangumi\" -> purifySpaceSet.contains(\"tab.bangumi\")\n                                \"cheese\" -> purifySpaceSet.contains(\"tab.cheese\")\n                                else -> false\n                            }\n                        }\n                    }\n                }\n\n                ogvApiResponseClass -> if (sPrefs.getBoolean(\"allow_download\", false)) {\n                    result.getObjectFieldOrNullAs<ArrayList<*>>(\"result\")?.forEach {\n                        it?.setIntField(\"isPlayable\", 1)\n                    }\n                }\n\n                ogvApiResponseV2Class -> if (sPrefs.getBoolean(\"allow_download\", false)) {\n                    result.getObjectFieldOrNull(\"data\")\n                        ?.getObjectFieldOrNullAs<MutableList<*>>(\"epPlayableParams\")\n                        ?.forEach { it?.setIntField(\"playableType\", 0) }\n                }\n\n                dmAdvertClass -> if (hidden && sPrefs.getBoolean(\"block_up_rcmd_ads\", false))\n                    result.setObjectField(\"ads\", null)\n\n                shareChannelsClass -> if (unlockPlayLimit) runCatchingOrNull {\n                    val belowChannels = result.getObjectFieldAs<ArrayList<Any>?>(\"belowChannels\")\n                        .takeUnless { it.isNullOrEmpty() } ?: return@hookAfterMethod\n                    var alreadyHas = false\n                    var toInsertIdx = -1\n                    belowChannels.forEachIndexed { idx, item ->\n                        val shareChannel = item.getObjectFieldAs<String?>(\"shareChannel\")\n                        if (shareChannel == \"PLAY_BACKGROUND_OFF\")\n                            toInsertIdx = idx\n                        if (shareChannel == \"LISTEN\")\n                            alreadyHas = true\n                    }\n                    if (alreadyHas || toInsertIdx == -1)\n                        return@hookAfterMethod\n                    val listenChannel = channelItemClass?.new()\n                        ?.setObjectField(\"name\", \"听视频\")\n                        ?.setObjectField(\"shareChannel\", \"LISTEN\")\n                        ?.setObjectField(\n                            \"picture\",\n                            \"https://i0.hdslb.com/bfs/share/f88d8c420a59ff1ca5975b38722408056e7337b7.png\"\n                        ) ?: return@hookAfterMethod\n                    belowChannels.add(toInsertIdx, listenChannel)\n                }\n            }\n        }\n\n        val searchRankClass = \"com.bilibili.search.api.SearchRank\".findClassOrNull(mClassLoader)\n        val searchGuessClass =\n            \"com.bilibili.search.api.SearchReferral\\$Guess\".findClassOrNull(mClassLoader)\n        val searchRankV2Class = \"com.bilibili.search2.api.SearchRank\".findClassOrNull(mClassLoader)\n        val searchGuessV2Class = \"com.bilibili.search2.api.SearchReferral\\$Guess\".findClassOrNull(mClassLoader)\n\n        instance.fastJsonClass?.hookAfterMethod(\n            \"parseArray\",\n            String::class.java,\n            Class::class.java\n        ) { param ->\n            @Suppress(\"UNCHECKED_CAST\")\n            val result = param.result as? MutableList<Any>\n            when (param.args[1] as Class<*>) {\n                searchRankClass, searchGuessClass, searchRankV2Class, searchGuessV2Class ->\n                    if (sPrefs.getBoolean(\"purify_search\", false) && sPrefs.getBoolean(\n                            \"hidden\",\n                            false\n                        )\n                    ) {\n                        result?.clear()\n                    }\n            }\n        }\n\n        instance.fastJsonClass?.hookAfterMethod(\n            instance.fastJsonParse(),\n            String::class.java,\n            Type::class.java,\n            \"com.alibaba.fastjson.parser.Feature[]\"\n        ) { param ->\n            var result = param.result ?: return@hookAfterMethod\n            if (result.javaClass == instance.generalResponseClass)\n                result = result.getObjectField(\"data\") ?: return@hookAfterMethod\n\n            when (result.javaClass) {\n                liveShoppingInfoClass -> {\n                    if (hidden && purifyLivePopups.contains(\"shoppingCard\")) {\n                        result.setObjectField(\"shoppingCardDetail\", null)\n                        result.runCatchingOrNull {\n                            setObjectField(\"recommendCardDetail\", null)\n                        }\n                    }\n                    if (hidden && purifyLivePopups.contains(\"shoppingSelected\"))\n                        result.runCatchingOrNull {\n                            setObjectField(\"selectedGoods\", null)\n                        }\n                }\n\n                liveGoodsCardInfoClass, liveRecommendCardGoodsClass -> {\n                    if (hidden && purifyLivePopups.contains(\"shoppingCard\"))\n                        param.result = null\n                }\n\n                biliLiveRoomInfoClass -> {\n                    if (hidden && purifyLivePopups.contains(\"follow\"))\n                        result.getObjectFieldOrNull(\"functionCard\")\n                            ?.setObjectField(\"followCard\", null)\n                    if (hidden && purifyLivePopups.contains(\"banner\"))\n                        result.setObjectField(\"bannerInfo\", null)\n                    if (hidden && sPrefs.getBoolean(\"no_live_mask\", false))\n                        result.setObjectField(\"areaMaskInfo\", null)\n                }\n\n                liveRoomRecommendCardClass -> {\n                    if (hidden && purifyLivePopups.contains(\"follow\"))\n                        param.result = null\n                }\n\n                liveRoomReserveInfoClass -> {\n                    if (hidden && purifyLivePopups.contains(\"reserve\"))\n                        result.setBooleanField(\"showReserveDetail\", false)\n                }\n\n                biliLiveRoomUserInfoClass -> {\n                    if (hidden && purifyLivePopups.contains(\"gift\"))\n                        result.getObjectFieldOrNull(\"functionCard\")\n                            ?.setObjectField(\"sengGiftCard\", null)\n                    if (hidden && purifyLivePopups.contains(\"task\"))\n                        result.runCatchingOrNull {\n                            setObjectField(\"taskInfo\", null)\n                        }\n                }\n\n                liveShoppingGotoBuyInfoClass -> {\n                    if (hidden && purifyLivePopups.contains(\"gotoBuy\"))\n                        param.result = null\n                }\n            }\n        }\n    }\n\n    private fun configTab(data: Any?, tabClass: Class<*>?) {\n        val tab = data?.getObjectFieldAs<MutableList<Any>>(\"tab\") ?: return\n        if (tabClass == null) return\n\n        var hasBangumiCN = false\n        var hasBangumiTW = false\n        var hasMovieCN = false\n        var hasMovieTW = false\n        var hasKoreaHK = false\n        var hasKoreaTW = false\n        tab.forEach {\n            when (it.getObjectFieldAs<String>(\"uri\")) {\n                \"bilibili://pgc/bangumi_v2\",\n                \"bilibili://pgc/home\" -> hasBangumiCN = true\n\n                \"bilibili://following/home_activity_tab/6544\" -> hasBangumiTW = true\n\n                \"bilibili://pgc/cinema_v2\",\n                \"bilibili://pgc/home?home_flow_type=2\" -> hasMovieCN = true\n\n                \"bilibili://following/home_activity_tab/168644\" -> hasMovieTW = true\n                \"bilibili://following/home_activity_tab/163541\" -> hasKoreaHK = true\n                \"bilibili://following/home_activity_tab/95636\" -> hasKoreaTW = true\n            }\n        }\n\n        if (sPrefs.getBoolean(\"add_bangumi\", false)) {\n            if (!hasBangumiCN) {\n                val bangumiCN = tabClass.new()\n                    .setObjectField(\"tabId\", \"50\")\n                    .setObjectField(\"name\", \"追番（大陸）\")\n                    .setObjectField(\"uri\", \"bilibili://pgc/home\")\n                    .setObjectField(\"reportId\", \"bangumi\")\n                    .setIntField(\"pos\", 50)\n                tab.add(bangumiCN)\n            }\n            if (!hasBangumiTW) {\n                val bangumiTW = tabClass.new()\n                    .setObjectField(\"tabId\", \"60\")\n                    .setObjectField(\"name\", \"追番（港澳台）\")\n                    .setObjectField(\"uri\", \"bilibili://following/home_activity_tab/6544\")\n                    .setObjectField(\"reportId\", \"bangumi\")\n                    .setIntField(\"pos\", 60)\n                tab.add(bangumiTW)\n            }\n        }\n\n        if (sPrefs.getBoolean(\"add_movie\", false)) {\n            if (!hasMovieCN) {\n                val movieCN = tabClass.new()\n                    .setObjectField(\"tabId\", \"70\")\n                    .setObjectField(\"name\", \"影視（大陸）\")\n                    .setObjectField(\"uri\", \"bilibili://pgc/home?home_flow_type=2\")\n                    .setObjectField(\"reportId\", \"film\")\n                    .setIntField(\"pos\", 70)\n                tab.add(movieCN)\n            }\n            if (!hasMovieTW) {\n                val movieTW = tabClass.new()\n                    .setObjectField(\"tabId\", \"80\")\n                    .setObjectField(\"name\", \"戏剧（港澳台）\")\n                    .setObjectField(\"uri\", \"bilibili://following/home_activity_tab/168644\")\n                    .setObjectField(\"reportId\", \"jptv\")\n                    .setIntField(\"pos\", 80)\n                tab.add(movieTW)\n            }\n        }\n\n        if (sPrefs.getBoolean(\"add_korea\", false)) {\n            if (!hasKoreaHK) {\n                val koreaHK = tabClass.new()\n                    .setObjectField(\"tabId\", \"803\")\n                    .setObjectField(\"name\", \"韩综（港澳）\")\n                    .setObjectField(\"uri\", \"bilibili://following/home_activity_tab/163541\")\n                    .setObjectField(\"reportId\", \"koreavhk\")\n                    .setIntField(\"pos\", 803)\n                tab.add(koreaHK)\n            }\n            if (!hasKoreaTW) {\n                val koreaTW = tabClass.new()\n                    .setObjectField(\"tabId\", \"804\")\n                    .setObjectField(\"name\", \"韩综（台湾）\")\n                    .setObjectField(\"uri\", \"bilibili://following/home_activity_tab/95636\")\n                    .setObjectField(\"reportId\", \"koreavtw\")\n                    .setIntField(\"pos\", 804)\n                tab.add(koreaTW)\n            }\n        }\n\n        val purifytabset = sPrefs.getStringSet(\"customize_home_tab\", emptySet())!!\n        if (purifytabset.isEmpty()) return\n        tab.removeAll {\n            when (it.getObjectFieldAs<String>(\"uri\")) {\n                \"bilibili://live/home\"\n                -> purifytabset.contains(\"live\")\n\n                \"bilibili://pegasus/promo\"\n                -> purifytabset.contains(\"promo\")\n\n                \"bilibili://pegasus/hottopic\"\n                -> purifytabset.contains(\"hottopic\")\n\n                \"bilibili://pgc/bangumi_v2\",\n                \"bilibili://pgc/home\",\n                \"bilibili://following/home_activity_tab/6544\"\n                -> purifytabset.contains(\"bangumi\")\n\n                \"bilibili://pgc/cinema_v2\",\n                \"bilibili://pgc/home?home_flow_type=2\",\n                \"bilibili://following/home_activity_tab/168644\"\n                -> purifytabset.contains(\"movie\")\n\n                \"bilibili://following/home_activity_tab/95636\",\n                \"bilibili://following/home_activity_tab/163541\"\n                -> purifytabset.contains(\"korea\")\n\n                else -> purifytabset.contains(\"other_tabs\")\n            }\n        }\n    }\n\n    data class BottomItem(\n        val name: String?,\n        val uri: String?,\n        val id: String?,\n        var showing: Boolean\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/KillDelayBootHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.Constant\nimport me.iacn.biliroaming.utils.hookAfterMethod\nimport me.iacn.biliroaming.utils.packageName\n\nclass KillDelayBootHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        instance.gripperBootExpClass?.hookAfterMethod(\n            if (packageName == Constant.PLAY_PACKAGE_NAME) \"b\" else \"getDelayMillis\"\n        ) { param ->\n            param.result = -1L\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.net.Uri\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.BuildConfig\nimport me.iacn.biliroaming.utils.*\nimport org.json.JSONArray\nimport org.json.JSONObject\n\nclass LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    companion object {\n        private const val TAG = \"LiveQualityHook\"\n    }\n\n    @Volatile\n    private var newQn: String = \"\"\n\n    private fun debug(msg: () -> String) {\n        if (BuildConfig.DEBUG) {\n            Log.d(\"[$TAG] - ${msg()}\")\n        }\n    }\n\n    override fun startHook() {\n        val liveQuality = sPrefs.getString(\"live_quality\", \"0\")?.toIntOrNull() ?: 0\n        if (liveQuality <= 0) {\n            return\n        }\n\n        val canSwitchLiveRoom = !sPrefs.getBoolean(\"forbid_switch_live_room\", false)\n\n        instance.defaultRequestInterceptClass?.hookBeforeAllMethods(instance.interceptMethod()) { param ->\n            val request = param.args[0]\n            val httpUrl = request.getObjectField(instance.urlField())\n            val url = httpUrl.toString()\n            if (!url.startsWith(\"https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo?\")) {\n                return@hookBeforeAllMethods\n            }\n\n            debug { \"oldHttpUrl: $url\" }\n\n            val uri = Uri.parse(httpUrl.toString())\n            val qn = uri.getQueryParameter(\"qn\")\n            if (qn.isNullOrEmpty() || qn == \"0\") {\n                val builder = uri.buildUpon().clearQuery()\n                for (name in uri.queryParameterNames) {\n                    if (name == \"qn\") {\n                        builder.appendQueryParameter(name, newQn.ifEmpty { liveQuality.toString() })\n                    } else {\n                        builder.appendQueryParameter(name, uri.getQueryParameter(name))\n                    }\n                }\n                val newHttpUrl = instance.httpUrlClass?.callStaticMethod(\n                    instance.httpUrlParseMethod(),\n                    builder.build().toString()\n                )\n                debug { \"newHttpUrl: $newHttpUrl\" }\n                request.setObjectField(instance.urlField(), newHttpUrl)\n            }\n        }\n\n        instance.retrofitResponseClass?.hookBeforeAllConstructors { param ->\n            val url = getRetrofitUrl(param.args[0]) ?: return@hookBeforeAllConstructors\n            val body = param.args[1] ?: return@hookBeforeAllConstructors\n\n            when {\n                instance.generalResponseClass?.isInstance(body) != true -> Unit\n                // 处理上下滑动切换直播间\n                url.startsWith(\"https://api.live.bilibili.com/xlive/app-interface/v2/room/recList?\") && canSwitchLiveRoom -> {\n                    val data = body.getObjectField(\"data\") ?: return@hookBeforeAllConstructors\n                    val info = JSONObject(\n                        instance.fastJsonClass?.callStaticMethod(\"toJSONString\", data).toString()\n                    )\n                    if (fixLiveRoomFeedInfo(info, liveQuality)) {\n                        body.setObjectField(\n                            \"data\",\n                            instance.fastJsonClass?.callStaticMethod(\n                                instance.fastJsonParse(),\n                                info.toString(),\n                                data.javaClass\n                            )\n                        )\n                    }\n                }\n\n                BuildConfig.DEBUG && url.startsWith(\"https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo?\") -> {\n                    val data = body.getObjectField(\"data\") ?: return@hookBeforeAllConstructors\n                    val info = JSONObject(\n                        instance.fastJsonClass?.callStaticMethod(\"toJSONString\", data).toString()\n                    )\n                    printCodec(info)\n                }\n            }\n        }\n\n        instance.liveRTCSourceServiceImplClass?.hookBeforeAllMethods(instance.switchAutoMethod()) { param ->\n            val mode = param.args[0] as? Enum<*> ?: return@hookBeforeAllMethods\n            if (mode.ordinal == 2) { // AUTO\n                param.result = null\n            }\n        }\n\n        instance.livePlayUrlSelectUtilClass?.hookBeforeMethod(\n            instance.buildSelectorDataMethod(),\n            Uri::class.java\n        ) { param ->\n            val originalUri = param.args[0] as Uri\n            if (!originalUri.isLive()) {\n                return@hookBeforeMethod\n            }\n\n            debug { \"originalLiveUrl: $originalUri\" }\n\n            if (originalUri.getQueryParameter(\"no_playurl\") == \"1\") {\n                newQn = liveQuality.toString()\n            } else {\n                newQn = findQuality(\n                    JSONArray(originalUri.getQueryParameter(\"accept_quality\")),\n                    liveQuality\n                ).toString()\n\n                param.args[0] = originalUri.modified(\n                    removeIf = { name ->\n                        name.startsWith(\"playurl\")\n                    },\n                    append = mapOf(\"no_playurl\" to \"1\")\n                ).also {\n                    debug { \"newLiveUrl: $it\" }\n                }\n            }\n\n            debug { \"newQn: $newQn\" }\n        }\n    }\n\n    private fun Uri.isLive(): Boolean {\n        return scheme in arrayOf(\"http\", \"https\")\n                && host == \"live.bilibili.com\"\n                && pathSegments.firstOrNull()?.all { it.isDigit() } == true\n    }\n\n    private fun findQuality(acceptQuality: JSONArray, expectQuality: Int): Int {\n        val acceptQnList = acceptQuality.asSequence<Int>().sorted().toList()\n        val max = acceptQnList.max()\n        val min = acceptQnList.min()\n        return when {\n            expectQuality > max -> max\n            expectQuality < min -> min\n            else -> acceptQnList.first { it >= expectQuality }\n        }\n    }\n\n    private fun Uri.modified(removeIf: (String) -> Boolean, append: Map<String, Any>): Uri {\n        val newBuilder = buildUpon().clearQuery()\n        for (name in queryParameterNames) {\n            val value = getQueryParameter(name) ?: \"\"\n            if (!removeIf(name)) {\n                newBuilder.appendQueryParameter(name, value)\n            }\n        }\n        append.forEach { (k, v) ->\n            newBuilder.appendQueryParameter(k, v.toString())\n        }\n        return newBuilder.build()\n    }\n\n    private fun fixLiveRoomFeedInfo(info: JSONObject, expectQuality: Int): Boolean {\n        val feedList = info.optJSONArray(\"list\") ?: return false\n        feedList.iterator().forEach { feedData ->\n            debug { \"oldFeedData: ${feedData.toString(2)}\" }\n            val newQuality = findQuality(feedData.getJSONArray(\"accept_quality\"), expectQuality)\n            feedData.apply {\n                put(\"current_qn\", newQuality)\n                put(\"current_quality\", newQuality)\n                put(\"play_url\", \"\")\n                put(\"play_url_h265\", \"\")\n                put(\"playurl_infos\", JSONArray())\n            }\n            debug { \"newFeedData: ${feedData.toString(2)}\" }\n        }\n        return true\n    }\n\n    private fun printCodec(info: JSONObject) {\n        val playUrlInfo = info.optJSONObject(\"playurl_info\") ?: return\n        val playUrlObj = playUrlInfo.getJSONObject(\"playurl\")\n        debug { \"printCodec >>>>>>>>>>>>>>\" }\n        playUrlObj.getJSONArray(\"stream\").iterator().forEach { stream ->\n            stream.getJSONArray(\"format\").iterator().forEach { format ->\n                format.getJSONArray(\"codec\").iterator().forEach {\n                    debug { \"codec: ${it.toString(2)}\" }\n                }\n            }\n        }\n        debug { \"printCodec <<<<<<<<<<<<<<\" }\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/LiveRoomHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.os.Bundle\nimport android.view.MotionEvent\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass LiveRoomHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (sPrefs.getBoolean(\"forbid_switch_live_room\", false)) {\n            instance.livePagerRecyclerViewClass?.replaceMethod(\n                \"onInterceptTouchEvent\",\n                MotionEvent::class.java\n            ) { false }\n        }\n        if (sPrefs.getBoolean(\"disable_live_room_double_click\", false)) {\n            instance.liveRoomPlayerViewClass?.declaredMethods?.find { it.name == \"onDoubleTap\" }\n                ?.hookBeforeMethod { param ->\n                    runCatching {\n                        val player = param.thisObject.callMethod(\"getPlayerCommonBridge\")\n                            ?: return@hookBeforeMethod\n                        val method = if (player.callMethodAs(\"isPlaying\"))\n                            \"pause\" else \"resume\"\n                        player.callMethod(method)\n                    }.onSuccess { param.result = true }\n                }\n            val lastTouchUpTimeField =\n                instance.liveRoomPlayerViewClass?.findFirstFieldByExactTypeOrNull(Long::class.javaPrimitiveType!!)\n            if (lastTouchUpTimeField != null) {\n                instance.liveRoomPlayerViewClass?.declaredMethods?.filter { m ->\n                    m.isPublic && m.returnType == Void.TYPE && m.parameterTypes.let { it.size == 1 && it[0] == MotionEvent::class.java }\n                }?.forEach { m ->\n                    m.hookAfterMethod { lastTouchUpTimeField.setLong(it.thisObject, 0L) }\n                }\n            }\n        }\n        if (!sPrefs.getBoolean(\"revert_live_room_feed\", false)) {\n            return\n        }\n\n        instance.liveKvConfigHelperClass?.hookAfterMethod(\n            \"getLocalValue\",\n            String::class.java\n        ) { param ->\n            if (param.args[0] == \"live_new_room_setting\") {\n                if (param.result != null) {\n                    val obj = (param.result as String).toJSONObject()\n                    if (obj.get(\"all_new_room_enable\") == \"2\") {\n                        obj.put(\"all_new_room_enable\", \"0\")\n                        param.result = obj.toString()\n                    }\n                }\n            }\n        }\n        instance.liveRoomActivityClass?.hookBeforeMethod(\n            \"onCreate\",\n            Bundle::class.java\n        ) { param ->\n            val intent = (param.thisObject as android.app.Activity).intent\n            if (intent.getStringExtra(\"is_room_feed\") == \"1\") {\n                intent.putExtra(\"is_room_feed\", \"0\")\n                Log.toast(\"已强制直播间使用旧版样式\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/LongPressSpeed.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.hookAfterAllConstructors\nimport me.iacn.biliroaming.utils.sPrefs\n\n\nclass LongPressSpeed(cl: ClassLoader) : BaseHook(cl) {\n    private val speed: Float = sPrefs.getInt(\"long_press_speed\", 300) / 100f\n\n\n    override fun startHook() {\n        if (speed == 3f) return\n\n        instance.tripleSpeedServiceClass!!.hookAfterAllConstructors {\n            val obj = it.thisObject\n            obj::class.java.getDeclaredField(\"\\$speed\").apply {\n                isAccessible = true\n                set(obj, speed)\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/MultiWindowHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.hookBeforeAllMethods\nimport me.iacn.biliroaming.utils.replaceMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass MultiWindowHook(mClassLoader: ClassLoader) : BaseHook(mClassLoader) {\n    override fun startHook() {\n        if (sPrefs.getBoolean(\"fake_non_multiwindow\", false).not()) return\n        Log.d(\"startHook: MultiWindowHook\")\n        Activity::class.java\n            .getDeclaredMethod(\"isInMultiWindowMode\")\n            .replaceMethod { false }\n        Activity::class.java\n            .hookBeforeAllMethods(\"onMultiWindowModeChanged\") { param ->\n                param.args[0] = false\n            }\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/MusicNotificationHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport de.robv.android.xposed.XposedHelpers\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.findClassOrNull\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass MusicNotificationHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"music_notification\", false)) return\n\n        Log.d(\"startHook: MusicNotification\")\n\n        \"com.bilibili.lib.blconfig.ConfigManager\\$Companion\".findClassOrNull(mClassLoader)?.run {\n            hookBeforeMethod(\n                \"isHitFF\",\n                String::class.java\n            ) { param ->\n                (param.args[0] as String).run {\n                    if (this == \"ff_background_use_system_media_controls\") {\n                        param.result = true\n                    }\n                }\n            }\n        }\n\n        \"com.bilibili.lib.dd.DeviceDecision\".findClassOrNull(mClassLoader)?.hookBeforeMethod(\n            \"getBoolean\",\n            String::class.java,\n            Boolean::class.javaPrimitiveType\n        ) { param ->\n            if (param.args[0] == \"dd_enable_system_media_control\") {\n                param.result = true\n            }\n        }\n\n        // Play store\n        \"com.bilibili.lib.blconfig.ConfigManager\\$a\".findClassOrNull(mClassLoader)?.run {\n            XposedHelpers.findMethodExactIfExists(this, \"g\", String::class.java)?.hookBeforeMethod {\n                (it.args[0] as String).run {\n                    if (this == \"ff_background_use_system_media_controls\") {\n                        it.result = true\n                    }\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/P2pHook.kt",
    "content": "package me.iacn.biliroaming.hook\r\n\r\nimport android.content.Context\r\nimport android.os.Bundle\r\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\r\nimport me.iacn.biliroaming.utils.*\r\n\r\nclass P2pHook(classLoader: ClassLoader) : BaseHook(classLoader) {\r\n    private val blockPcdn = sPrefs.getBoolean(\"block_pcdn\", false)\r\n    private val blockPcdnLive = sPrefs.getBoolean(\"block_pcdn_live\", false)\r\n    override fun startHook() {\r\n        if (!blockPcdn && !blockPcdnLive) return\r\n        Log.d(\"startHook: P2P\")\r\n        \"tv.danmaku.ijk.media.player.IjkMediaPlayer\\$IjkMediaPlayerServiceConnection\".from(\r\n            mClassLoader\r\n        )?.replaceMethod(\"initP2PClient\") {}\r\n        if (blockPcdn) {\r\n            \"tv.danmaku.ijk.media.player.P2P\".from(mClassLoader)?.run {\r\n                hookBeforeMethod(\"getInstance\", Context::class.java, Bundle::class.java) { param ->\r\n                    param.args[0] = null\r\n                    param.args[1].callMethod(\"clear\")\r\n                }\r\n                hookBeforeConstructor(Context::class.java, Bundle::class.java) { param ->\r\n                    param.args[0] = null\r\n                    param.args[1].callMethod(\"clear\")\r\n                }\r\n            }\r\n        }\r\n        if (blockPcdnLive) {\r\n            instance.liveRtcEnable()?.let {\r\n                instance.liveRtcEnableClass?.replaceMethod(it) { false }\r\n            }\r\n            \"com.bilibili.bililive.playercore.p2p.P2PType\".from(mClassLoader)?.run {\r\n                hookBeforeMethod(\"create\", Int::class.javaPrimitiveType) { it.args[0] = 0 }\r\n                hookBeforeMethod(\"createTo\", Int::class.javaPrimitiveType) { it.args[0] = 0 }\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/PegasusHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport de.robv.android.xposed.XC_MethodHook\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport me.iacn.biliroaming.utils.json.FastjsonHelper\nimport me.iacn.biliroaming.utils.json.GsonHelper\nimport me.iacn.biliroaming.utils.json.JsonHelper\nimport me.iacn.biliroaming.utils.json.getObjectAs\nimport me.iacn.biliroaming.utils.json.getObjectAsHelperOrNull\nimport me.iacn.biliroaming.utils.json.toJsonHelper\n\nclass PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    private val hidden = sPrefs.getBoolean(\"hidden\", false)\n\n    private val filterSet = sPrefs.getStringSet(\"customize_home_recommend\", emptySet()).orEmpty()\n\n    private val hideLowPlayCountLimit = sPrefs.getLong(\"hide_low_play_count_recommend_limit\", 0)\n    private val hideShortDurationLimit = sPrefs.getInt(\"hide_short_duration_recommend_limit\", 0)\n    private val hideLongDurationLimit = sPrefs.getInt(\"hide_long_duration_recommend_limit\", 0)\n    private val kwdFilterTitleList by lazy {\n        migrateHomeFilterPrefsIfNeeded()\n        sPrefs.getStringSet(\"home_filter_keywords_title\", null).orEmpty()\n    }\n    private val kwdFilterTitleRegexes by lazy { kwdFilterTitleList.map { it.toRegex() } }\n    private val kwdFilterTitleRegexMode = sPrefs.getBoolean(\"home_filter_title_regex_mode\", false)\n    private val kwdFilterReasonList by lazy {\n        migrateHomeFilterPrefsIfNeeded()\n        sPrefs.getStringSet(\"home_filter_keywords_reason\", null).orEmpty()\n    }\n    private val kwdFilterReasonRegexes by lazy { kwdFilterReasonList.map { it.toRegex() } }\n    private val kwdFilterReasonRegexMode = sPrefs.getBoolean(\"home_filter_reason_regex_mode\", false)\n    private val kwdFilterUidList by lazy {\n        migrateHomeFilterPrefsIfNeeded()\n        sPrefs.getStringSet(\"home_filter_keywords_uid\", null)\n            ?.mapNotNull { it.toLongOrNull() }.orEmpty()\n    }\n    private val kwdFilterUpnameList by lazy {\n        migrateHomeFilterPrefsIfNeeded()\n        sPrefs.getStringSet(\"home_filter_keywords_up\", null).orEmpty()\n    }\n    private val kwdFilterUpnameRegexes by lazy { kwdFilterUpnameList.map { it.toRegex() } }\n    private val kwdFilterUpnameRegexMode = sPrefs.getBoolean(\"home_filter_up_regex_mode\", false)\n    private val kwdFilterRnameList by lazy {\n        migrateHomeFilterPrefsIfNeeded()\n        sPrefs.getStringSet(\"home_filter_keywords_category\", null).orEmpty()\n    }\n    private val kwdFilterTnameList by lazy {\n        migrateHomeFilterPrefsIfNeeded()\n        sPrefs.getStringSet(\"home_filter_keywords_channel\", null).orEmpty()\n    }\n    private val disableAutoRefresh = sPrefs.getBoolean(\"disable_auto_refresh\", false)\n    private val removeRelatePromote = sPrefs.getBoolean(\"remove_video_relate_promote\", false)\n    private val removeRelateOnlyAv = sPrefs.getBoolean(\"remove_video_relate_only_av\", false)\n    private val removeRelateNothing = sPrefs.getBoolean(\"remove_video_relate_nothing\", false)\n    private val applyToRelate = sPrefs.getBoolean(\"home_filter_apply_to_relate\", false)\n\n    private val hideTopEntrance = sPrefs.getBoolean(\"hide_top_entrance_popular\", false)\n    private val hideSuggestFollow = sPrefs.getBoolean(\"hide_suggest_follow_popular\", false)\n    private val hideTopicList = sPrefs.getBoolean(\"hide_topic_list_popular\", true)\n\n    private val filterMap = mapOf(\n        \"advertisement\" to listOf(\"ad\", \"cm\", \"cm_v2\"),\n        \"article\" to listOf(\"article\"),\n        \"bangumi\" to listOf(\"bangumi\", \"special\", \"pgc\"),\n        \"game\" to listOf(\"game\"),\n        \"picture\" to listOf(\"picture\"),\n        \"vertical\" to listOf(\"vertical\", \"story\"),\n        \"banner\" to listOf(\"banner\"),\n        \"live\" to listOf(\"live\"),\n        \"inline\" to listOf(\"inline\"),\n        \"notify\" to listOf(\"notify_tunnel\"),\n        \"large_cover\" to listOf(\"large_cover\"),\n        \"middle_cover\" to listOf(\"middle_cover\"),\n        \"small_cover\" to listOf(\"small_cover\"),\n    )\n\n    private val filter = filterSet.flatMap {\n        filterMap[it].orEmpty()\n    }\n\n    val handleCoverRatio = sPrefs.getString(\"pegasus_cover_ratio\", \"0\")?.toFloat() ?: 0f\n\n    companion object {\n        private const val REASON_ID_TITLE = 1145140L\n        private const val REASON_ID_RCMD_REASON = 1145141L\n        private const val REASON_ID_UP_ID = 1145142L\n        private const val REASON_ID_UP_NAME = 1145143L\n        private const val REASON_ID_CATEGORY_NAME = 1145144L\n        private const val REASON_ID_CHANNEL_NAME = 1145145L\n        private val blockReasonIds = arrayOf(\n            REASON_ID_TITLE,\n            REASON_ID_RCMD_REASON,\n            REASON_ID_UP_ID,\n            REASON_ID_UP_NAME,\n            REASON_ID_CATEGORY_NAME,\n            REASON_ID_CHANNEL_NAME,\n        )\n\n        private var popularDataVersion = \"\"\n        private var popularDataCount = 0L\n    }\n\n    private fun String.isNum() = isNotEmpty() && all { it.isDigit() }\n\n    private fun String.toPlayCount() = runCatchingOrNull {\n        when {\n            isNum() -> toDouble().toLong()\n            contains(\"万\") -> (replace(\"万\", \"\").toDouble() * 10_000).toLong()\n            contains(\"亿\") -> (replace(\"亿\", \"\").toDouble() * 100_000_000).toLong()\n            else -> -1L\n        }\n    } ?: -1L\n\n    // 屏蔽过低的播放数\n    private fun isLowCountVideo(obj: JsonHelper): Boolean {\n        if (hideLowPlayCountLimit == 0L) return false\n        val text = obj.runCatchingOrNull {\n            getObjectAs<String?>(\"cover_left_text_1\")\n        }.orEmpty()\n        return text.toPlayCount().let {\n            if (it == -1L) false\n            else it < hideLowPlayCountLimit\n        }\n    }\n    private fun isLowCountVideoUnite(count: Long): Boolean {\n        if (hideLowPlayCountLimit == 0L) return false\n        return count < hideLowPlayCountLimit\n    }\n\n    // 屏蔽指定播放时长\n    private fun durationVideo(obj: JsonHelper): Boolean {\n        if (hideLongDurationLimit == 0 && hideShortDurationLimit == 0)\n            return false\n        val duration = obj.getObjectAsHelperOrNull(\"player_args\")\n            ?.getObjectAs(\"duration\") ?: 0\n        if (hideLongDurationLimit != 0 && duration > hideLongDurationLimit)\n            return true\n        return hideShortDurationLimit != 0 && duration < hideShortDurationLimit\n    }\n    private fun durationVideoUnite(duration: Long): Boolean {\n        if (hideLongDurationLimit == 0 && hideShortDurationLimit == 0)\n            return false\n        if (hideLongDurationLimit != 0 && duration > hideLongDurationLimit)\n            return true\n        return hideShortDurationLimit != 0 && duration < hideShortDurationLimit\n    }\n\n    private fun isContainsBlockKwd(obj: JsonHelper): Boolean {\n        // 屏蔽标题关键词\n        if (kwdFilterTitleList.isNotEmpty()) {\n            val title = obj.getObjectAs<String?>(\"title\").orEmpty()\n            if (kwdFilterTitleRegexMode && title.isNotEmpty()) {\n                if (kwdFilterTitleRegexes.any { title.contains(it) })\n                    return true\n            } else if (title.isNotEmpty()) {\n                if (kwdFilterTitleList.any { title.contains(it) })\n                    return true\n            }\n        }\n\n        // 屏蔽UID\n        if (kwdFilterUidList.isNotEmpty()) {\n            val uid = obj.getObjectAsHelperOrNull(\"args\")?.getObjectAs<Long>(\"up_id\") ?: 0L\n            if (uid != 0L && kwdFilterUidList.any { it == uid })\n                return true\n        }\n\n        // 屏蔽UP主\n        if (kwdFilterUpnameList.isNotEmpty()) {\n            val upname = if (obj.getObjectAs<String?>(\"goto\") == \"picture\") {\n                obj.runCatchingOrNull { getObjectAs<String?>(\"desc\") }.orEmpty()\n            } else {\n                obj.getObjectAsHelperOrNull(\"args\")?.getObjectAs<String?>(\"up_name\").orEmpty()\n            }\n            if (kwdFilterUpnameRegexMode && upname.isNotEmpty()) {\n                if (kwdFilterUpnameRegexes.any { upname.contains(it) })\n                    return true\n            } else if (upname.isNotEmpty()) {\n                if (kwdFilterUpnameList.any { upname.contains(it) })\n                    return true\n            }\n        }\n\n        // 屏蔽分区\n        if (kwdFilterRnameList.isNotEmpty()) {\n            val rname = obj.getObjectAsHelperOrNull(\"args\")\n                ?.getObjectAs<String?>(\"rname\").orEmpty()\n            if (rname.isNotEmpty() && kwdFilterRnameList.any { rname.contains(it) })\n                return true\n        }\n\n        // 屏蔽频道\n        if (kwdFilterTnameList.isNotEmpty()) {\n            val tname = obj.getObjectAsHelperOrNull(\"args\")\n                ?.getObjectAs<String?>(\"tname\").orEmpty()\n            if (tname.isNotEmpty() && kwdFilterTnameList.any { tname.contains(it) })\n                return true\n        }\n\n        // 屏蔽推荐关键词（可能不存在，必须放最后）\n        if (kwdFilterReasonList.isNotEmpty()) {\n            val reason = obj.runCatchingOrNull {\n                getObjectAsHelperOrNull(\"rcmd_reason\")?.getObjectAs<String?>(\"text\").orEmpty()\n            }.orEmpty()\n            if (kwdFilterReasonRegexMode && reason.isNotEmpty()) {\n                if (kwdFilterReasonRegexes.any { reason.contains(it) })\n                    return true\n            } else if (reason.isNotEmpty()) {\n                if (kwdFilterReasonList.any { reason.contains(it) })\n                    return true\n            }\n        }\n\n        return false\n    }\n\n    private fun isContainsBlockKwdUnite(card: Any): Boolean {\n        if (card.callMethodAs(\"hasBasicInfo\")) {\n            card.callMethodAs<Any>(\"getBasicInfo\").let { basicInfo ->\n                // 屏蔽标题关键词\n                if (kwdFilterTitleList.isNotEmpty()) {\n                    val title = basicInfo.callMethodAs<String>(\"getTitle\")\n                    if (kwdFilterTitleRegexMode && title.isNotEmpty()) {\n                        if (kwdFilterTitleRegexes.any { title.contains(it) })\n                            return true\n                    } else if (title.isNotEmpty()) {\n                        if (kwdFilterTitleList.any { title.contains(it) })\n                            return true\n                    }\n                }\n\n                basicInfo.callMethodAs<Any>(\"getAuthor\").let { author ->\n                    // 屏蔽UID\n                    if (kwdFilterUidList.isNotEmpty()) {\n                        val uid = author.callMethodAs<Long>(\"getMid\")\n                        if (uid != 0L && kwdFilterUidList.any { it == uid })\n                            return true\n                    }\n\n                    // 屏蔽UP主\n                    if (kwdFilterUpnameList.isNotEmpty()) {\n                        val upName = author.callMethodAs<String>(\"getTitle\")\n                        if (kwdFilterUpnameRegexMode && upName.isNotEmpty()) {\n                            if (kwdFilterUpnameRegexes.any { upName.contains(it) })\n                                return true\n                        } else if (upName.isNotEmpty()) {\n                            if (kwdFilterUpnameList.any { upName.contains(it) })\n                                return true\n                        }\n                    }\n                }\n            }\n        }\n        return false\n    }\n\n    private fun ArrayList<Any>.appendReasons(jsonHelper: Class<JsonHelper>) = this.forEach {\n        val item = it.toJsonHelper(jsonHelper)\n        val title = item.getObjectAs<String?>(\"title\").orEmpty()\n        val rcmdReason = item.runCatchingOrNull {\n            getObjectAsHelperOrNull(\"rcmd_reason\")?.getObjectAs<String?>(\"text\")\n        }.orEmpty()\n        val args = item.getObjectAsHelperOrNull(\"args\")\n        val upId = args?.getObjectAs<Long>(\"up_id\") ?: 0L\n        val upName = if (item.getObject(\"goto\") == \"picture\") {\n            item.runCatchingOrNull { getObjectAs<String?>(\"desc\") }.orEmpty()\n        } else {\n            args?.getObjectAs<String?>(\"up_name\").orEmpty()\n        }\n        val categoryName = args?.getObjectAs<String?>(\"rname\").orEmpty()\n        val channelName = args?.getObjectAs<String?>(\"tname\").orEmpty()\n        val treePoint = item.getObjectAs<MutableList<Any>?>(\"three_point_v2\")\n        val reasons = mutableListOf<Any>()\n        instance.treePointItemClass?.new()?.apply {\n            setObjectField(\"title\", \"漫游屏蔽\")\n            setObjectField(\"subtitle\", \"（本地屏蔽，重启生效，可前往首页推送过滤器查看）\")\n            setObjectField(\"type\", \"dislike\")\n            if (title.isNotEmpty()) {\n                instance.dislikeReasonClass?.new()?.apply {\n                    setLongField(\"id\", REASON_ID_TITLE)\n                    setObjectField(\"name\", \"标题:$title\")\n                }?.let { reasons.add(it) }\n            }\n            if (rcmdReason.isNotEmpty()) {\n                instance.dislikeReasonClass?.new()?.apply {\n                    setLongField(\"id\", REASON_ID_RCMD_REASON)\n                    setObjectField(\"name\", \"推荐原因:$rcmdReason\")\n                }?.let { reasons.add(it) }\n            }\n            if (upId != 0L) {\n                instance.dislikeReasonClass?.new()?.apply {\n                    setLongField(\"id\", REASON_ID_UP_ID)\n                    setObjectField(\"name\", \"UID:$upId\")\n                }?.let { reasons.add(it) }\n            }\n            if (upName.isNotEmpty()) {\n                instance.dislikeReasonClass?.new()?.apply {\n                    setLongField(\"id\", REASON_ID_UP_NAME)\n                    setObjectField(\"name\", \"UP主:$upName\")\n                }?.let { reasons.add(it) }\n            }\n            if (categoryName.isNotEmpty()) {\n                instance.dislikeReasonClass?.new()?.apply {\n                    setLongField(\"id\", REASON_ID_CATEGORY_NAME)\n                    setObjectField(\"name\", \"分区:$categoryName\")\n                }?.let { reasons.add(it) }\n            }\n            if (channelName.isNotEmpty()) {\n                instance.dislikeReasonClass?.new()?.apply {\n                    setLongField(\"id\", REASON_ID_CHANNEL_NAME)\n                    setObjectField(\"name\", \"频道:$channelName\")\n                }?.let { reasons.add(it) }\n            }\n            setObjectField(\"reasons\", reasons)\n        }?.let {\n            if (treePoint != null && reasons.isNotEmpty()) {\n                treePoint.add(it)\n            } else if (reasons.isNotEmpty()) {\n                item.setObjectField(\"threePoint\", mutableListOf(it))\n            }\n        }\n    }\n\n    private fun Any.disableAutoRefresh() {\n        if (!disableAutoRefresh) return\n        // only exist on android and android_i now\n        runCatchingOrNull {\n            setLongField(\"autoRefreshTimeByActive\", -1L)\n            setLongField(\"autoRefreshTimeByAppear\", -1L)\n            setIntField(\"autoRefreshTimeByBehavior\", -1)\n        }\n        // only exist on android now\n        runCatchingOrNull {\n            setIntField(\"autoRefreshByBehavior\", -1)\n        }\n        // only exist on android_hd now\n        runCatchingOrNull {\n            setIntField(\"auto_refresh_time\", 0)\n        }\n    }\n\n    private fun Any.customSmallCoverWhRatio() {\n        if (handleCoverRatio == 0f) return\n        runCatchingOrNull {\n            setFloatField(\"smallCoverWhRatio\", handleCoverRatio)\n        }\n    }\n\n    private fun isPromoteRelate(item: Any) = removeRelatePromote\n            && (item.callMethod(\"getFromSourceType\") == 2L ||\n            item.callMethod(\"getGoto\") == \"cm\")\n\n    private fun isNotAvRelate(item: Any) = removeRelatePromote && removeRelateOnlyAv\n            && item.callMethod(\"getGoto\") != \"av\"\n\n    private fun isLowCountRelate(item: Any): Boolean {\n        if (hideLowPlayCountLimit == 0L) return false\n        val text = item.callMethod(\"getStatV2\")?.callMethod(\"getViewVt\")\n            ?.callMethodAs<String>(\"getText\").orEmpty()\n        return text.toPlayCount().let {\n            if (it == -1L) false\n            else it < hideLowPlayCountLimit\n        }\n    }\n\n    private fun isDurationInvalidRelate(item: Any): Boolean {\n        if (hideLongDurationLimit == 0 && hideShortDurationLimit == 0)\n            return false\n        val duration = item.callMethodAs(\"getDuration\") ?: 0L\n        if (hideLongDurationLimit != 0 && duration > hideLongDurationLimit)\n            return true\n        return hideShortDurationLimit != 0 && duration < hideShortDurationLimit\n    }\n\n    private fun isContainsBlockKwdRelate(item: Any): Boolean {\n        if (kwdFilterTitleList.isNotEmpty()) {\n            val title = item.callMethodAs<String>(\"getTitle\")\n            if (kwdFilterTitleRegexMode && title.isNotEmpty()) {\n                if (kwdFilterTitleRegexes.any { title.contains(it) })\n                    return true\n            } else if (title.isNotEmpty()) {\n                if (kwdFilterTitleList.any { title.contains(it) })\n                    return true\n            }\n        }\n        if (kwdFilterUidList.isNotEmpty()) {\n            val uid = item.callMethod(\"getAuthor\")\n                ?.callMethodAs(\"getMid\") ?: 0L\n            if (uid != 0L && kwdFilterUidList.any { it == uid })\n                return true\n        }\n        if (kwdFilterUpnameList.isNotEmpty()) {\n            val upname = item.callMethod(\"getAuthor\")\n                ?.callMethodAs<String>(\"getName\").orEmpty()\n            if (kwdFilterUpnameRegexMode && upname.isNotEmpty()) {\n                if (kwdFilterUpnameRegexes.any { upname.contains(it) })\n                    return true\n            } else if (upname.isNotEmpty()) {\n                if (kwdFilterUpnameList.any { upname.contains(it) })\n                    return true\n            }\n        }\n        if (kwdFilterReasonList.isNotEmpty()) {\n            val reason = item.callMethod(\"getRcmdReasonStyle\")\n                ?.callMethodAs<String>(\"getText\").orEmpty()\n            if (kwdFilterReasonRegexMode && reason.isNotEmpty()) {\n                if (kwdFilterReasonRegexes.any { reason.contains(it) })\n                    return true\n            } else if (reason.isNotEmpty()) {\n                if (kwdFilterReasonList.any { reason.contains(it) })\n                    return true\n            }\n        }\n        return false\n    }\n\n    private fun isLowCountVideoPopular(obj: Any): Boolean {\n        if (hideLowPlayCountLimit == 0L) return false\n        val rightDesc2 = obj.callMethodAs<String>(\"getRightDesc2\")\n\n        val text = rightDesc2.split(' ').first().removeSuffix(\"观看\")\n        return text.toPlayCount().let {\n            if (it == -1L) false\n            else it < hideLowPlayCountLimit\n        }\n    }\n\n    private fun durationVideoPopular(obj: Any): Boolean {\n        fun getTimeInSeconds(time: String): Long {\n            val parts = time.split(\":\").map { it.toInt() }\n\n            val seconds: Long = when (parts.size) {\n                2 -> parts[0] * 60L + parts[1]\n                3 -> parts[0] * 3600L + parts[1] * 60L + parts[2]\n                else -> Long.MAX_VALUE\n            }\n\n            return seconds\n        }\n\n        if (hideLongDurationLimit == 0 && hideShortDurationLimit == 0)\n            return false\n        val text = obj.callMethodAs<String>(\"getCoverRightText1\")\n        val duration = getTimeInSeconds(text)\n\n        if (hideLongDurationLimit != 0 && duration > hideLongDurationLimit)\n            return true\n        return hideShortDurationLimit != 0 && duration < hideShortDurationLimit\n    }\n\n    private fun isContainsBlockKwdPopular(obj: Any, base: Any?): Boolean {\n        base?: return false\n\n        val threePointV4 = base.callMethod(\"getThreePointV4\")\n        val sharePlane = threePointV4?.callMethod(\"getSharePlane\")\n\n        if (kwdFilterTitleList.isNotEmpty()) {\n            val title = base.callMethodAs<String>(\"getTitle\")\n            if (kwdFilterTitleRegexMode && title.isNotEmpty()) {\n                if (kwdFilterTitleRegexes.any { title.contains(it) })\n                    return true\n            } else if (title.isNotEmpty()) {\n                if (kwdFilterTitleList.any { title.contains(it) }) {\n                    return true\n                }\n            }\n        }\n\n        if (kwdFilterUidList.isNotEmpty()) {\n            val uid = sharePlane?.callMethodAs<Long>(\"getAuthorId\") ?: 0\n            if (uid != 0L && kwdFilterUidList.any { it == uid })\n                return true\n        }\n\n        if (kwdFilterUpnameList.isNotEmpty()) {\n            val upname = obj.callMethodAs<String>(\"getRightDesc1\")\n            if (kwdFilterUpnameRegexMode && upname.isNotEmpty()) {\n                if (kwdFilterUpnameRegexes.any { upname.contains(it) })\n                    return true\n            } else if (upname.isNotEmpty()) {\n                if (kwdFilterUpnameList.any { upname.contains(it) })\n                    return true\n            }\n        }\n\n        if (kwdFilterReasonList.isNotEmpty()) {\n            val reasonStyle = obj.callMethod(\"getRcmdReasonStyle\")\n            val reasonText = reasonStyle?.callMethodAs<String>(\"getText\")\n\n            do {\n                if (reasonText.isNullOrEmpty()) {\n                    break\n                }\n                if (kwdFilterReasonRegexMode && kwdFilterReasonRegexes.any { reasonText.contains(it) }) {\n                    return true\n                } else if (kwdFilterReasonList.any { reasonText.contains(it) }) {\n                    return true\n                }\n            } while (false)\n        }\n\n        return false\n    }\n    private fun cardV5Handle(obj: Any?): Boolean {\n        obj ?: return false\n\n        val base = obj.callMethod(\"getBase\")\n        if (popularDataCount % 10 == 0L) {\n            popularDataVersion = base?.callMethodAs<String>(\"getParam\") ?: popularDataVersion\n        }\n\n        return isLowCountVideoPopular(obj) || durationVideoPopular(obj) || isContainsBlockKwdPopular(obj, base)\n    }\n\n    override fun startHook() {\n        Log.d(\"startHook: Pegasus\")\n\n        fun hookPegasusFeedConvert(json: JsonHelper) {\n            json.getObject(\"config\")!!.apply {\n                disableAutoRefresh()\n                customSmallCoverWhRatio()\n            }\n            if (!hidden) return\n            json.getObjectAs<ArrayList<Any>>(\"items\").run {\n                removeAll {\n                    val item = it.toJsonHelper(json.javaClass)\n                    val cardGoto = item.getObjectAs<String?>(\"card_goto\").orEmpty()\n                    val cardType = item.getObjectAs<String?>(\"card_type\").orEmpty()\n                    val goto = item.getObjectAs<String?>(\"goto\").orEmpty()\n                    filter.any {\n                        it in cardGoto || it in cardType || it in goto\n                    } || isLowCountVideo(item) || isContainsBlockKwd(item) || durationVideo(item)\n                }\n                appendReasons(json.javaClass)\n            }\n        }\n\n        instance.pegasusFeedClass?.hookAfterMethod(\n            \"convert\",\n            Object::class.java\n        ) { param ->\n            val data = param.result?.getObjectField(\"data\") ?: return@hookAfterMethod\n            val json = data.toJsonHelper<FastjsonHelper>()\n            hookPegasusFeedConvert(json)\n        }\n\n        instance.pegasusParserClass?.hookAfterMethod(\n            \"convert\",\n            Object::class.java\n        ) { param ->\n            val data = param.result?.getObjectField(\"data\") ?: return@hookAfterMethod\n            val json = data.toJsonHelper<GsonHelper>()\n            hookPegasusFeedConvert(json)\n        }\n\n        if (!hidden) return\n        fun MutableList<Any>.filter() = removeAll {\n            isPromoteRelate(it) || isNotAvRelate(it) || (applyToRelate && (isLowCountRelate(it)\n                    || isDurationInvalidRelate(it) || isContainsBlockKwdRelate(it)))\n        }\n\n        fun MutableList<Any>.filterUnite() = removeAll {\n            val allowTypeList = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n            var shouldFiltered = false\n            allowTypeList.removeAll { digit ->\n                (removeRelateOnlyAv && digit != 1) || (removeRelatePromote && digit in listOf(\n                    3, // Resource, like mall\n                    4, // GAME\n                    5, // CM\n                    10 // SPECIAL\n                ))\n            }\n            // av filter\n            if (applyToRelate) {\n                if (it.callMethodAs(\"hasAv\")) {\n                    it.callMethodAs<Any>(\"getAv\").let { av ->\n                        val duration = av.callMethodAs<Long>(\"getDuration\")\n                        if (durationVideoUnite(duration)) {\n                            shouldFiltered = true\n                            return@let\n                        }\n                        if (av.callMethodAs(\"hasStat\")) {\n                            av.callMethodAs<Any>(\"getStat\").let { stat ->\n                                if (stat.callMethodAs(\"hasVt\")) {\n                                    stat.callMethodAs<Any>(\"getVt\").let { vt ->\n                                        if (isLowCountVideoUnite(vt.callMethodAs<Long>(\"getValue\"))) {\n                                            shouldFiltered = true\n                                            return@let\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        // rcmd_reason 推荐理由\n                        if (av.callMethodAs(\"hasRcmdReason\")) {\n                            av.callMethodAs<Any>(\"getRcmdReason\").let { rcmd ->\n                                if (kwdFilterReasonList.isNotEmpty()) {\n                                    val reason = rcmd.callMethodAs<String>(\"getText\")\n                                    if (kwdFilterReasonRegexMode && reason.isNotEmpty()) {\n                                        if (kwdFilterReasonRegexes.any { reason.contains(it) })\n                                            shouldFiltered = true\n                                            return@let\n                                    } else if (reason.isNotEmpty()) {\n                                        if (kwdFilterReasonList.any { reason.contains(it) })\n                                            shouldFiltered = true\n                                            return@let\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    if (isContainsBlockKwdUnite(it)) {\n                        shouldFiltered = true\n                    }\n                }\n            }\n            removeRelateNothing || it.callMethodAs(\"getRelateCardTypeValue\") !in allowTypeList || shouldFiltered\n        }\n\n        instance.viewMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeView\" else \"view\",\n            instance.viewReqClass\n        ) { param ->\n            param.result ?: return@hookAfterMethod\n            if (removeRelatePromote && removeRelateOnlyAv && removeRelateNothing) {\n                param.result.callMethod(\"clearRelates\")\n                param.result.callMethod(\"clearPagination\")\n                return@hookAfterMethod\n            }\n            param.result.callMethod(\"ensureRelatesIsMutable\")\n            param.result.callMethodAs<MutableList<Any>>(\"getRelatesList\").filter()\n        }\n        instance.viewMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeRelatesFeed\" else \"relatesFeed\",\n            \"com.bapis.bilibili.app.view.v1.RelatesFeedReq\"\n        ) { param ->\n            param.result ?: return@hookAfterMethod\n            param.result.callMethod(\"ensureListIsMutable\")\n            param.result.callMethodAs<MutableList<Any>>(\"getListList\").filter()\n        }\n\n        instance.viewUniteMossClass?.run {\n            hookAfterMethod(\n                if (instance.useNewMossFunc) \"executeView\" else \"view\",\n                instance.viewUniteReqClass\n            ) { param ->\n                param.result ?: return@hookAfterMethod\n                param.result.callMethod(\"getTab\")?.run {\n                    callMethod(\"ensureTabModuleIsMutable\")\n                    callMethodAs<MutableList<Any>>(\"getTabModuleList\").map { originalTabModules ->\n                        if (!originalTabModules.callMethodAs<Boolean>(\"hasIntroduction\")) return@map\n                        originalTabModules.callMethodAs<Any>(\"getIntroduction\").run {\n                            callMethod(\"ensureModulesIsMutable\")\n                            callMethodAs<MutableList<Any>>(\"getModulesList\").map { module ->\n                                if (!module.callMethodAs<Boolean>(\"hasRelates\")) return@map\n                                module.callMethodAs<Any>(\"getRelates\").run {\n                                    callMethod(\"ensureCardsIsMutable\")\n                                    callMethodAs<MutableList<Any>>(\"getCardsList\").filterUnite()\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            hookAfterMethod(\n                if (instance.useNewMossFunc) \"executeRelatesFeed\" else \"relatesFeed\",\n                \"com.bapis.bilibili.app.viewunite.v1.RelatesFeedReq\"\n            ) { param ->\n                param.result?.run {\n                    callMethod(\"ensureRelatesIsMutable\")\n                    callMethodAs<MutableList<Any>>(\"getRelatesList\").filterUnite()\n                }\n            }\n        }\n\n        fun hookDislikeReason(reason: Any?): Boolean {\n            if (reason == null || reason.getLongField(\"id\") !in blockReasonIds)\n                return false\n            val id = reason.getLongField(\"id\")\n            val name = reason.getObjectFieldAs<String?>(\"name\").orEmpty()\n            val value = name.substringAfter(\":\")\n            when (id) {\n                REASON_ID_TITLE -> {\n                    val validValue =\n                        if (kwdFilterTitleRegexMode) Regex.escape(value) else value\n                    sPrefs.appendStringForSet(\"home_filter_keywords_title\", validValue)\n                }\n\n                REASON_ID_RCMD_REASON -> {\n                    val validValue =\n                        if (kwdFilterReasonRegexMode) Regex.escape(value) else value\n                    sPrefs.appendStringForSet(\"home_filter_keywords_reason\", validValue)\n                }\n\n                REASON_ID_UP_ID -> {\n                    sPrefs.appendStringForSet(\"home_filter_keywords_uid\", value)\n                }\n\n                REASON_ID_UP_NAME -> {\n                    val validValue =\n                        if (kwdFilterUpnameRegexMode) Regex.escape(value) else value\n                    sPrefs.appendStringForSet(\"home_filter_keywords_up\", validValue)\n                }\n\n                REASON_ID_CATEGORY_NAME -> {\n                    sPrefs.appendStringForSet(\"home_filter_keywords_category\", value)\n                }\n\n                REASON_ID_CHANNEL_NAME -> {\n                    sPrefs.appendStringForSet(\"home_filter_keywords_channel\", value)\n                }\n            }\n            Log.toast(\"添加成功\", force = true)\n            return true\n        }\n\n        instance.cardClickProcessorClass?.declaredMethods\n            ?.find { it.name == instance.onFeedClicked() }?.hookBeforeMethod { param ->\n                if (hookDislikeReason(param.args[2])) {\n                    param.result = null\n                }\n            }\n\n        \"com.bilibili.pegasus.ext.threepoint.ThreePointKt\".findClassOrNull(mClassLoader)\n            ?.declaredMethods?.find { it.parameterTypes.size == 8 }\n            ?.hookBeforeMethod {\n                if (hookDislikeReason(it.args[3])) {\n                    it.result = null\n                }\n            }\n\n        fun MutableList<Any>.filterPopular() = removeIf {\n            when (it.callMethod(\"getItemCase\")?.toString()) {\n                \"SMALL_COVER_V5\" -> {\n                    val v5 = it.callMethod(\"getSmallCoverV5\")\n                    popularDataCount++\n                    return@removeIf cardV5Handle(v5)\n                }\n                \"POPULAR_TOP_ENTRANCE\" -> {\n                    return@removeIf hideTopEntrance\n                }\n                \"RCMD_ONE_ITEM\" -> {\n                    popularDataCount++\n                    return@removeIf hideSuggestFollow\n                }\n                \"TOPIC_LIST\" -> {\n                    popularDataCount++\n                    return@removeIf hideTopicList\n                }\n                else -> {\n                    popularDataCount++\n                    return@removeIf false\n                }\n            }\n        }\n\n        instance.popularClass?.hookBeforeMethod(\n            if (instance.useNewMossFunc) \"executeIndex\" else \"index\",\n            \"com.bapis.bilibili.app.show.popular.v1.PopularResultReq\"\n        ) { param ->\n            param.args ?: return@hookBeforeMethod\n\n            val idx = param.args[0].getLongFieldOrNull(\"idx_\")\n            if (idx == null || idx == 0L) {\n                popularDataCount = 0\n                popularDataVersion = \"\"\n                return@hookBeforeMethod\n            }\n\n            param.args[0].setObjectField(\"lastParam_\", popularDataVersion)\n            param.args[0].setLongField(\"idx_\", popularDataCount)\n        }\n        instance.popularClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeIndex\" else \"index\",\n            \"com.bapis.bilibili.app.show.popular.v1.PopularResultReq\"\n        ) { param ->\n            param.result ?: return@hookAfterMethod\n\n            param.result.callMethod(\"ensureItemsIsMutable\")\n            param.result.callMethodAs<MutableList<Any>>(\"getItemsList\").filterPopular()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/PlayArcConfHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.net.Uri\nimport me.iacn.biliroaming.*\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport kotlin.math.ceil\n\nclass PlayArcConfHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    private val playURLMoss get() = instance.playURLMossClass?.new()\n    private val viewMoss get() = instance.viewMossClass?.new()\n\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"play_arc_conf\", false)) return\n\n        instance.playURLMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executePlayView\" else \"playView\", instance.playViewReqClass\n        ) { param ->\n            param.result?.callMethod(\"getPlayArc\")?.run {\n                arrayOf(\n                    callMethod(\"getCastConf\"),\n                    callMethod(\"getBackgroundPlayConf\"),\n                    callMethod(\"getSmallWindowConf\")\n                ).forEach {\n                    it?.callMethod(\"setDisabled\", false)\n                    it?.callMethod(\"setIsSupport\", true)\n                }\n            }\n        }\n        val supportedArcConf = \"com.bapis.bilibili.playershared.ArcConf\"\n            .from(mClassLoader)?.new()?.apply {\n                callMethod(\"setDisabled\", false)\n                callMethod(\"setIsSupport\", true)\n            }\n        val arcConfs = \"com.bapis.bilibili.playershared.PlayArcConf\"\n                .from(mClassLoader)?.callStaticMethod(\"getDefaultInstance\")\n        arcConfs?.callMethodAs<LinkedHashMap<Int, Any?>>(\"internalGetMutableArcConfs\")\n                ?.run {\n                    // CASTCONF,BACKGROUNDPLAY,SMALLWINDOW,LISTEN\n                    intArrayOf(2, 9, 23, 36).forEach { this[it] = supportedArcConf }\n                }\n\n        instance.playerMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executePlayViewUnite\" else \"playViewUnite\",\n            instance.playViewUniteReqClass\n        ) { param ->\n            param.result?.callMethod(\"mergePlayArcConf\", arcConfs)\n        }\n        instance.playerMossClass?.hookBeforeMethod(\"playViewUnite\", instance.playViewUniteReqClass, instance.mossResponseHandlerClass) { param ->\n            param.args[1] = param.args[1].mossResponseHandlerProxy { resp ->\n                resp?.callMethod(\"mergePlayArcConf\", arcConfs)\n            }\n        }\n        \"com.bapis.bilibili.app.listener.v1.ListenerMoss\".from(mClassLoader)?.run {\n            val playlistHook = { param: MethodHookParam ->\n                val req = param.args[0]\n                param.args[1] = param.args[1].mossResponseHandlerProxy { resp ->\n                    runCatching {\n                        reconstructPlaylistResponse(req, resp)\n                    }.onFailure {\n                        Log.e(it)\n                        Log.toast(\"听视频解锁失败\")\n                    }\n                }\n            }\n            hookBeforeMethod(\n                \"playlist\",\n                \"com.bapis.bilibili.app.listener.v1.PlaylistReq\",\n                instance.mossResponseHandlerClass,\n                hooker = playlistHook\n            )\n            hookBeforeMethod(\n                \"rcmdPlaylist\",\n                \"com.bapis.bilibili.app.listener.v1.RcmdPlaylistReq\",\n                instance.mossResponseHandlerClass,\n                hooker = playlistHook\n            )\n            hookBeforeMethod(\n                \"playHistory\",\n                \"com.bapis.bilibili.app.listener.v1.PlayHistoryReq\",\n                instance.mossResponseHandlerClass,\n                hooker = playlistHook\n            )\n            hookAfterMethod(\n                if (instance.useNewMossFunc) \"executePlayURL\" else \"playURL\",\n                \"com.bapis.bilibili.app.listener.v1.PlayURLReq\"\n            ) { param ->\n                if (instance.networkExceptionClass?.isInstance(param.throwable) == true)\n                    return@hookAfterMethod\n                val resp = param.result ?: \"com.bapis.bilibili.app.listener.v1.PlayURLResp\"\n                    .on(mClassLoader).new()\n                val playable = resp.callMethodAs<Int>(\"getPlayable\")\n                val playerInfoMap = resp.callMethodAs<Map<*, *>>(\"getPlayerInfoMap\")\n                if (playable == 0 && playerInfoMap.isNotEmpty())\n                    return@hookAfterMethod\n                Log.toast(\"听视频解锁中\")\n                param.result = reconstructPlayUrlResponse(param.args[0], resp)\n            }\n        }\n    }\n\n    private fun reconstructPlaylistResponse(req: Any, resp: Any?) {\n        resp ?: return\n        val needPartItems = mutableListOf<Any>()\n        resp.callMethodAs<List<Any>>(\"getListList\").filter {\n            it.callMethodAs<Int>(\"getPlayable\") != 0\n        }.forEach {\n            it.callMethod(\"setPlayable\", 0)\n            if (it.callMethodAs<Int>(\"getPartsCount\") <= 0)\n                needPartItems.add(it)\n        }\n        if (needPartItems.isEmpty())\n            return\n        val playerArgs = runCatchingOrNull {\n            PlayerArgs.parseFrom(\n                req.callMethod(\"getPlayerArgs\")\n                    ?.callMethodAs<ByteArray>(\"toByteArray\")\n            )\n        } ?: playerArgs {\n            fnval = BangumiPlayUrlHook.MAX_FNVAL.toLong()\n            forceHost = 2\n            qn = 64\n        }\n        val commViewReq = viewReq {\n            fnval = playerArgs.fnval.toInt()\n            forceHost = playerArgs.forceHost.toInt()\n            fourk = 1\n            this.playerArgs = playerArgs\n            qn = playerArgs.qn.toInt()\n        }.toByteArray().let {\n            instance.viewReqClass?.callStaticMethod(\"parseFrom\", it)\n        } ?: return\n        val bkArcPartClass = instance.bkArcPartClass ?: return\n        for (item in needPartItems) {\n            val oid = item.callMethod(\"getArc\")?.callMethodAs<Long>(\"getOid\")\n                ?: continue\n            val viewReq = commViewReq.apply { callMethod(\"setAid\", oid) }\n            val viewReply = ViewReply.parseFrom(\n                viewMoss?.callMethod(\"view\", viewReq)\n                    ?.callMethodAs<ByteArray>(\"toByteArray\") ?: continue\n            )\n            val parts = viewReply.pagesList.mapNotNull { p ->\n                bKArcPart {\n                    duration = p.page.duration\n                    this.oid = oid\n                    page = p.page.page\n                    subId = p.page.cid\n                    title = p.page.part\n                }.toByteArray().let {\n                    bkArcPartClass.callStaticMethod(\"parseFrom\", it)\n                }\n            }\n            item.callMethod(\"addAllParts\", parts)\n        }\n    }\n\n    private fun reconstructPlayUrlResponse(req: Any, resp: Any) = runCatching {\n        val reqObj = ListenPlayURLReq.parseFrom(req.callMethodAs<ByteArray>(\"toByteArray\"))\n        val commPlayViewReq = playViewReq {\n            epId = reqObj.item.oid\n            qn = reqObj.playerArgs.qn\n            fnval = reqObj.playerArgs.fnval.toInt()\n            forceHost = reqObj.playerArgs.forceHost.toInt()\n            fourk = true\n            preferCodecType = CodeType.CODE265\n        }.toByteArray().let {\n            instance.playViewReqClass?.callStaticMethod(\"parseFrom\", it)\n        } ?: return@runCatching resp\n        reqObj.item.subIdList.associateWith { subId ->\n            val playViewReq = commPlayViewReq.apply { callMethod(\"setCid\", subId) }\n            val playViewReply = UGCPlayViewReply.parseFrom(\n                playURLMoss?.callMethod(\"playView\", playViewReq)\n                    ?.callMethodAs<ByteArray>(\"toByteArray\") ?: return@associateWith null\n            )\n            listenPlayInfo {\n                var deadline = 0L\n                fnval = reqObj.playerArgs.fnval.toInt()\n                format = playViewReply.videoInfo.format\n                length = playViewReply.videoInfo.timelength\n                qn = playViewReply.videoInfo.quality\n                videoCodecid = playViewReply.videoInfo.videoCodecid\n                playDash = listenPlayDASH {\n                    playViewReply.videoInfo.dashAudioList.map {\n                        listenDashItem {\n                            id = it.id\n                            size = it.size\n                            bandwidth = it.bandwidth\n                            baseUrl = it.baseUrl\n                            backupUrl.addAll(it.backupUrlList)\n                            if (deadline == 0L)\n                                deadline = Uri.parse(baseUrl).getQueryParameter(\"deadline\")\n                                    ?.toLongOrNull() ?: 0\n                        }\n                    }.let { audio.addAll(it) }\n                    duration = ceil(playViewReply.videoInfo.timelength / 1000.0).toInt()\n                    minBufferTime = 0.0F\n                }\n                playViewReply.videoInfo.streamListList.map {\n                    formatDescription {\n                        it.streamInfo.let { si ->\n                            description = si.description\n                            displayDesc = si.displayDesc\n                            format = si.format\n                            quality = si.quality\n                            superscript = si.superscript\n                        }\n                    }\n                }.let { formats.addAll(it) }\n                expireTime = deadline\n            }\n        }.mapNotNull { it.value?.let { v -> it.key to v } }.toMap()\n            .takeIf { it.isNotEmpty() }?.let {\n                listenPlayURLResp {\n                    item = reqObj.item\n                    playable = 0\n                    playerInfo.putAll(it)\n                }.let {\n                    resp.javaClass.callStaticMethod(\"parseFrom\", it.toByteArray())\n                }\n            } ?: resp\n    }.onFailure {\n        Log.e(it)\n        Log.toast(\"听视频解锁失败\")\n    }.getOrDefault(resp)\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/PlayerLongPressHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.view.MotionEvent\nimport me.iacn.biliroaming.utils.*\n\nclass PlayerLongPressHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"forbid_player_long_click_accelerate\", false)) return\n\n        Log.d(\"startHook: PlayerLongPress\")\n\n        val hooker: Hooker = { param -> param.result = true }\n        // pre 6.59.0\n        \"tv.danmaku.biliplayerimpl.gesture.GestureService\\$mTouchListener\\$1\".findClassOrNull(\n            mClassLoader\n        )?.hookBeforeMethod(\n            \"onLongPress\", MotionEvent::class.java, hooker = hooker\n        )\n        // post 6.59.0\n        arrayOf(\n            \"tv.danmaku.biliplayerimpl.gesture.GestureService\\$initInnerLongPressListener\\$1\\$onLongPress\\$1\",\n            \"tv.danmaku.biliplayerimpl.gesture.GestureService\\$initInnerLongPressListener\\$1\\$onLongPressEnd\\$1\",\n            // post 7.32.0\n            \"com.bilibili.playerbizcommon.gesture.GestureService\\$initInnerLongPressListener\\$1\\$onLongPress\\$1\",\n            \"com.bilibili.playerbizcommon.gesture.GestureService\\$initInnerLongPressListener\\$1\\$onLongPressEnd\\$1\"\n        ).forEach { className ->\n            className.findClassOrNull(mClassLoader)\n                ?.hookBeforeMethod(\"invoke\", Object::class.java, hooker = hooker)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/ProtoBufHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass ProtoBufHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    companion object {\n        fun Any.removeCmdDms() {\n            callMethod(\"clearActivityMeta\")\n            runCatchingOrNull {\n                callMethod(\"clearCommand\")\n            }\n            setObjectField(\"unknownFields\", instance.unknownFieldSetLiteInstance)\n        }\n    }\n\n    private val mainListReplyClass by Weak { \"com.bapis.bilibili.main.community.reply.v1.MainListReply\" from mClassLoader }\n    private val emptyPageV1Class by Weak { \"com.bapis.bilibili.main.community.reply.v1.EmptyPage\" from mClassLoader }\n    private val textV1Class by Weak { \"com.bapis.bilibili.main.community.reply.v1.EmptyPage\\$Text\" from mClassLoader }\n    private val textStyleV1Class by Weak { \"com.bapis.bilibili.main.community.reply.v1.TextStyle\" from mClassLoader }\n    private val textV2Class by Weak { \"com.bapis.bilibili.main.community.reply.v2.EmptyPage\\$Text\" from mClassLoader }\n    private val textStyleV2Class by Weak { \"com.bapis.bilibili.main.community.reply.v2.TextStyle\" from mClassLoader }\n    private val videoGuideClass by Weak { \"com.bapis.bilibili.app.view.v1.VideoGuide\" from mClassLoader }\n\n\n    private val unlockPlayLimit = sPrefs.getBoolean(\"play_arc_conf\", false)\n    private val blockCommentGuide = sPrefs.getBoolean(\"block_comment_guide\", false)\n    private val disableMainPageStory = sPrefs.getBoolean(\"disable_main_page_story\", false)\n\n    // 以下为隐藏功能配置\n    private val hidden = sPrefs.getBoolean(\"hidden\", false)\n\n    private val removeHonor = hidden && sPrefs.getBoolean(\"remove_video_honor\", false)\n    private val removeUgcSeason = hidden && sPrefs.getBoolean(\"remove_video_UgcSeason\", false)\n    private val removeCmdDms = hidden && sPrefs.getBoolean(\"remove_video_cmd_dms\", false)\n    private val removeUpVipLabel = hidden && sPrefs.getBoolean(\"remove_up_vip_label\", false)\n\n    // 搜索过滤\n    private val searchFilterUid = run {\n        sPrefs.getStringSet(\"search_filter_keyword_uid\", null)\n            ?.mapNotNull { it.toLongOrNull() }.orEmpty()\n    }\n    private val searchFilterContents = run {\n        sPrefs.getStringSet(\"search_filter_keyword_content\", null).orEmpty()\n    }\n    private val searchFilterContentRegexes by lazy { searchFilterContents.map { it.toRegex() } }\n    private val searchFilterContentRegexMode = sPrefs.getBoolean(\"search_filter_content_regex_mode\", false)\n    private val searchFilterUpNames = run {\n        sPrefs.getStringSet(\"search_filter_keyword_upname\", null).orEmpty()\n    }\n    private val searchRemoveRelatePromote = sPrefs.getBoolean(\"search_filter_remove_relate_promote\", false)\n\n    // 评论过滤\n    private val commentFilterBlockAtComment = sPrefs.getBoolean(\"comment_filter_block_at_comment\", false)\n    private val commentFilterAtUid = run {\n        sPrefs.getStringSet(\"comment_filter_keyword_at_uid\", null)\n            ?.mapNotNull { it.toLongOrNull() }.orEmpty()\n    }\n    private val commentFilterContents = run {\n        sPrefs.getStringSet(\"comment_filter_keyword_content\", null).orEmpty()\n    }\n    private val commentFilterContentRegexes by lazy { commentFilterContents.map { it.toRegex() } }\n    private val commentFilterContentRegexMode = sPrefs.getBoolean(\"comment_filter_content_regex_mode\", false)\n    private val commentFilterAtUpNames = run {\n        sPrefs.getStringSet(\"comment_filter_keyword_at_upname\", null).orEmpty()\n    }\n    private val targetCommentAuthorLevel = sPrefs.getLong(\"target_comment_author_level\", 0L)\n\n    private val purifySearch = hidden && sPrefs.getBoolean(\"purify_search\", false)\n    private val purifyCity = hidden && sPrefs.getBoolean(\"purify_city\", false)\n    private val purifyCampus = hidden && sPrefs.getBoolean(\"purify_campus\", false)\n\n    private val blockLiveOrder = hidden && sPrefs.getBoolean(\"block_live_order\", false)\n    private val blockWordSearch = hidden && sPrefs.getBoolean(\"block_word_search\", false)\n    private val blockModules = hidden && sPrefs.getBoolean(\"block_modules\", false)\n    private val blockUpperRecommendAd = hidden && sPrefs.getBoolean(\"block_upper_recommend_ad\", false)\n    private val blockVideoComment = hidden && sPrefs.getBoolean(\"block_video_comment\", false)\n    private val blockViewPageAds = hidden && sPrefs.getBoolean(\"block_view_page_ads\", false)\n\n\n    override fun startHook() {\n\n        hookMossView()\n\n        if (hidden && (purifyCity || purifyCampus)) {\n            listOf(\n                \"com.bapis.bilibili.app.dynamic.v1.DynTabReply\",\n                \"com.bapis.bilibili.app.dynamic.v2.DynTabReply\"\n            ).forEach { clazz ->\n                clazz.hookAfterMethod(\n                    mClassLoader,\n                    \"getDynTabList\"\n                ) { param ->\n                    param.result = (param.result as List<*>).filterNot {\n                        purifyCity && it?.callMethodAs<Long>(\"getCityId\") != 0L\n                                || purifyCampus && it?.callMethodAs<String>(\"getAnchor\") == \"campus\"\n                    }\n                }\n            }\n        }\n\n        if (hidden && removeCmdDms) {\n            instance.viewMossClass?.hookAfterMethod(\n                if (instance.useNewMossFunc) \"executeViewProgress\" else \"viewProgress\",\n                \"com.bapis.bilibili.app.view.v1.ViewProgressReq\"\n            ) { param ->\n                param.result?.callMethod(\"setVideoGuide\", videoGuideClass?.new())\n            }\n            instance.viewUniteMossClass?.hookAfterMethod(\n                if (instance.useNewMossFunc) \"executeViewProgress\" else \"viewProgress\",\n                \"com.bapis.bilibili.app.viewunite.v1.ViewProgressReq\"\n            ) { param ->\n                param.result?.run {\n                    callMethod(\"clearDm\")\n                    callMethod(\"getVideoGuide\")?.callMethod(\"clearContractCard\")\n                }\n            }\n            instance.viewMossClass?.replaceMethod(\n                if (instance.useNewMossFunc) \"executeTFInfo\" else \"tFInfo\",\n                \"com.bapis.bilibili.app.view.v1.TFInfoReq\"\n            ) { null }\n            instance.dmMossClass?.hookAfterMethod(\n                if (instance.useNewMossFunc) \"executeDmView\" else \"dmView\",\n                instance.dmViewReqClass,\n            ) { it.result?.removeCmdDms() }\n        }\n        if (hidden && purifySearch) {\n            \"com.bapis.bilibili.app.interfaces.v1.SearchMoss\".hookAfterMethod(\n                mClassLoader,\n                if (instance.useNewMossFunc) \"executeDefaultWords\" else \"defaultWords\",\n                \"com.bapis.bilibili.app.interfaces.v1.DefaultWordsReq\"\n            ) { param ->\n                param.result = null\n            }\n        }\n        if (hidden && blockWordSearch) {\n            \"com.bapis.bilibili.main.community.reply.v1.Content\".hookAfterMethod(\n                mClassLoader,\n                \"internalGetUrls\"\n            ) { param ->\n                (param.result as LinkedHashMap<*, *>?)?.let { m ->\n                    val iterator = m.iterator()\n                    while (iterator.hasNext()) {\n                        iterator.next().value.callMethodAs<String?>(\"getAppUrlSchema\")\n                            ?.takeIf {\n                                it.startsWith(\"bilibili://search?from=appcommentline_search\")\n                            }?.run {\n                                iterator.remove()\n                            }\n                    }\n                }\n            }\n        }\n        if (hidden && blockModules) {\n            \"com.bapis.bilibili.app.resource.v1.ModuleMoss\".hookAfterMethod(\n                mClassLoader,\n                if (instance.useNewMossFunc) \"executeList\" else \"list\",\n                \"com.bapis.bilibili.app.resource.v1.ListReq\"\n            ) {\n                it.result?.callMethod(\"clearPools\")\n            }\n        }\n        if (hidden && blockUpperRecommendAd) {\n            \"com.bapis.bilibili.ad.v1.SourceContentDto\".from(mClassLoader)\n                ?.replaceMethod(\"getAdContent\") { null }\n        }\n        if (disableMainPageStory) {\n            \"com.bapis.bilibili.app.distribution.setting.experimental.MultipleTusConfig\"\n                .from(mClassLoader)?.hookAfterMethod(\"getTopLeft\") { param ->\n                    param.result?.runCatchingOrNull {\n                        callMethod(\"clearBadge\")\n                        callMethod(\"clearListenBackgroundImage\")\n                        callMethod(\"clearListenForegroundImage\")\n                        callMethod(\"clearStoryBackgroundImage\")\n                        callMethod(\"clearStoryForegroundImage\")\n                        val tabUrl = \"bilibili://root?tab_name=我的\"\n                        callMethod(\"getUrl\")?.callMethod(\"setValue\", tabUrl)\n                        callMethod(\"getUrlV2\")?.callMethod(\"setValue\", tabUrl)\n                        callMethod(\"getGoto\")?.callMethod(\"setValue\", \"1\")\n                        callMethod(\"getGotoV2\")?.callMethod(\"setValue\", 1)\n                    }\n                }\n        }\n        if (blockCommentGuide || (hidden && blockVideoComment)) {\n            \"com.bapis.bilibili.main.community.reply.v1.ReplyMoss\".hookBeforeMethod(\n                mClassLoader,\n                if (instance.useNewMossFunc) \"executeMainList\" else \"mainList\",\n                \"com.bapis.bilibili.main.community.reply.v1.MainListReq\",\n            ) { param ->\n                val type = param.args[0].callMethodAs<Long>(\"getType\")\n                if (hidden && blockVideoComment && type == 1L) {\n                    val reply = mainListReplyClass?.new()?.apply {\n                        val subjectControl = callMethod(\"getSubjectControl\")\n                        val emptyPage = emptyPageV1Class?.new()?.also {\n                            subjectControl?.callMethod(\"setEmptyPage\", it)\n                        }\n                        emptyPage?.callMethod(\n                            \"setImageUrl\",\n                            \"https://i0.hdslb.com/bfs/app-res/android/img_holder_forbid_style1.webp\"\n                        )\n                        textV1Class?.new()?.apply {\n                            callMethod(\"setRaw\", \"评论区已由漫游屏蔽\")\n                            textStyleV1Class?.new()?.apply {\n                                callMethod(\"setFontSize\", 14)\n                                callMethod(\"setTextDayColor\", \"#FF61666D\")\n                                callMethod(\"setTextNightColor\", \"#FFA2A7AE\")\n                            }?.let {\n                                callMethod(\"setStyle\", it)\n                            }\n                        }?.let {\n                            emptyPage?.callMethod(\"addTexts\", it)\n                        }\n                    }\n                    param.result = reply\n                    return@hookBeforeMethod\n                }\n                if (!blockCommentGuide) return@hookBeforeMethod\n                param.result.runCatchingOrNull {\n                    callMethod(\"getSubjectControl\")?.run {\n                        callMethod(\"clearEmptyBackgroundTextPlain\")\n                        callMethod(\"clearEmptyBackgroundTextHighlight\")\n                        callMethod(\"clearEmptyBackgroundUri\")\n                        callMethod(\"getEmptyPage\")?.let { page ->\n                            page.callMethod(\"clearLeftButton\")\n                            page.callMethod(\"clearRightButton\")\n                            page.callMethodAs<List<Any>>(\"getTextsList\").takeIf { it.size > 1 }\n                                ?.let {\n                                    page.callMethod(\"clearTexts\")\n                                    page.callMethod(\"addTexts\", it.first().apply {\n                                        callMethod(\"setRaw\", \"还没有评论哦\")\n                                    })\n                                }\n                        }\n                    }\n                }\n            }\n            \"com.bapis.bilibili.main.community.reply.v2.ReplyMoss\".from(mClassLoader)\n                ?.hookBeforeMethod(\n                    if (instance.useNewMossFunc) \"executeSubjectDescription\" else \"subjectDescription\",\n                    \"com.bapis.bilibili.main.community.reply.v2.SubjectDescriptionReq\"\n                ) { param ->\n                    val defaultText = textV2Class?.new()?.apply {\n                        val tipStr = if (hidden && blockVideoComment) {\n                            \"评论区已由漫游屏蔽\"\n                        } else {\n                            \"还没有评论哦\"\n                        }\n                        callMethod(\"setRaw\", tipStr)\n                        textStyleV2Class?.new()?.apply {\n                            callMethod(\"setFontSize\", 14)\n                            callMethod(\"setTextDayColor\", \"#FF61666D\")\n                            callMethod(\"setTextNightColor\", \"#FFA2A7AE\")\n                        }?.let {\n                            callMethod(\"setStyle\", it)\n                        }\n                    } ?: return@hookBeforeMethod\n                    param.result.runCatchingOrNull {\n                        callMethod(\"getEmptyPage\")?.run {\n                            callMethod(\"clearLeftButton\")\n                            callMethod(\"clearRightButton\")\n                            callMethod(\"ensureTextsIsMutable\")\n                            callMethodAs<MutableList<Any>>(\"getTextsList\").run {\n                                clear()\n                                add(defaultText)\n                            }\n                            if (!(hidden && blockVideoComment)) return@run\n                            callMethod(\n                                \"setImageUrl\",\n                                \"https://i0.hdslb.com/bfs/app-res/android/img_holder_forbid_style1.webp\"\n                            )\n                        }\n                    }\n                }\n        }\n        val needSearchFilter = hidden and (searchFilterContents.isNotEmpty() or searchFilterUid.isNotEmpty() or searchFilterUpNames.isNotEmpty()) or searchRemoveRelatePromote\n        if (needSearchFilter) {\n            instance.searchAllResponseClass?.hookAfterMethod(\"getItemList\") { p ->\n                val items = p.result as? List<Any?> ?: return@hookAfterMethod\n                p.result = items.filter { item ->\n                    val videoCard = item?.getObjectField(\"cardItem_\") ?: return@filter true\n                    if (instance.searchVideoCardClass?.isInstance(videoCard) == false) {\n                        if (searchRemoveRelatePromote) {\n                            if (item.callMethodAs<Boolean>(\"hasCm\")) return@filter false\n                            if (item.callMethodAs<Boolean>(\"hasSpecial\")) return@filter false\n                        }\n                        return@filter true\n                    }\n                    if (videoCard.getLongField(\"mid_\") in searchFilterUid) return@filter false\n                    if (videoCard.getObjectFieldAs<String>(\"author_\") in searchFilterUpNames) return@filter false\n                    if (searchFilterContentRegexMode) {\n                        if (searchFilterContentRegexes.any { it.containsMatchIn(videoCard.getObjectFieldAs<String>(\"title_\")) })\n                            return@filter false\n                    } else {\n                        if (searchFilterContents.any { videoCard.getObjectFieldAs<String>(\"title_\").contains(it) }) return@filter false\n                    }\n                    true\n                }\n            }\n        }\n\n        val needCommentFilter =\n            hidden && (commentFilterBlockAtComment || commentFilterContents.isNotEmpty() || commentFilterAtUid.isNotEmpty() || commentFilterAtUpNames.isNotEmpty() || targetCommentAuthorLevel != 0L)\n        if (needCommentFilter) {\n            val blockAtCommentSplitRegex = Regex(\"\\\\s+\")\n\n            fun Any.validCommentAuthorLevel(): Boolean {\n                if (targetCommentAuthorLevel == 0L) return true\n                val authorLevel = getObjectField(\"member_\")?.getObjectFieldAs<Long>(\"level_\") ?: 6L\n                return authorLevel >= targetCommentAuthorLevel\n            }\n\n            fun Any.validCommentContent(): Boolean {\n                val content = getObjectField(\"content_\") ?: return true\n                val commentMessage = content.getObjectFieldAs<String>(\"message_\")\n\n                val contentIsToBlock = commentFilterContents.isNotEmpty() && if (commentFilterContentRegexMode) {\n                    commentFilterContentRegexes.any { commentMessage.contains(it) }\n                } else {\n                    commentFilterContents.any { commentMessage.contains(it) }\n                }\n                if (contentIsToBlock) return false\n\n                if (commentFilterBlockAtComment && commentMessage.trim()\n                        .split(blockAtCommentSplitRegex).all { it.startsWith(\"@\") }\n                ) return false\n\n                if (commentFilterAtUpNames.isEmpty() && commentFilterAtUid.isEmpty()) return true\n                val atNameToMid = content.getObjectFieldAs<Map<String, Long>>(\"atNameToMid_\")\n                return !(atNameToMid.keys.any { it in commentFilterAtUpNames } || atNameToMid.values.any { it in commentFilterAtUid })\n            }\n\n            fun Any.filterComment() = validCommentAuthorLevel() && validCommentContent()\n\n            \"com.bapis.bilibili.main.community.reply.v1.MainListReply\".from(mClassLoader)\n                ?.hookAfterMethod(\"getRepliesList\") { p ->\n                    val l = p.result as? List<*> ?: return@hookAfterMethod\n                    p.result = l.filter { it?.filterComment() ?: true }\n                }\n            \"com.bapis.bilibili.main.community.reply.v1.ReplyInfo\".from(mClassLoader)\n                ?.hookAfterMethod(\"getRepliesList\") { p ->\n                    val l = p.result as? List<*> ?: return@hookAfterMethod\n                    p.result = l.filter { it?.filterComment() ?: true }\n                }\n        }\n\n        if (!sPrefs.getBoolean(\"replace_story_video\", false)) return\n        val disableBooleanValue = \"com.bapis.bilibili.app.distribution.BoolValue\".from(mClassLoader)?.callStaticMethod(\"getDefaultInstance\") ?: return\n        \"com.bapis.bilibili.app.distribution.setting.play.PlayConfig\".from(mClassLoader)?.run {\n            replaceAllMethods(\"getLandscapeAutoStory\") { disableBooleanValue }\n            replaceAllMethods(\"getShouldAutoStory\") { disableBooleanValue }\n        }\n    }\n\n    private fun hookMossView() {\n        instance.viewUniteMossClass?.hookAfterMethod(\n            \"executeView\",\n            instance.viewUniteReqClass\n        ) { param ->\n            param.result?.let {\n                handleViewReply(it, true)\n            }\n        }\n\n        instance.viewMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeView\" else \"view\",\n            instance.viewReqClass\n        ) { param ->\n            param.result?.let {\n                handleViewReply(it, false)\n            }\n        }\n    }\n\n    private fun handleViewReply(viewReply: Any, isUnite: Boolean) {\n        val aid = viewReply.callMethod(\"getArc\")\n            ?.callMethodAs(\"getAid\") ?: -1L\n        val like = viewReply.callMethod(\"getReqUser\")\n            ?.callMethodAs(\"getLike\") ?: -1\n\n        if (aid > 0 && like != -1) {\n            AutoLikeHook.detail = aid to like\n        }\n\n        BangumiPlayUrlHook.qnApplied.set(false)\n\n        // 视频详情页荣誉卡片\n        fun Any.isHonor() = removeHonor && callMethodAs(\"hasHonor\")\n\n        // 视频详情页活动卡片(含会员购等), 区分于视频下方推荐处的广告卡片\n        fun Any.isActivityEntranceModule() = blockViewPageAds && callMethodAs(\"hasActivityEntranceModule\")\n\n        // 视频详情页直播预约卡片\n        fun Any.isLiveOrder() = blockLiveOrder && callMethodAs(\"hasLiveOrder\")\n\n        // 视频下方分集列表\n        fun Any.isUgcSeason() = removeUgcSeason && callMethodAs(\"hasUgcSeason\")\n\n        fun Any.isLikeComment() = blockCommentGuide && callMethodAs(\"hasLikeComment\")\n\n        fun MutableList<Any>.filter() = removeAll {\n            it.isActivityEntranceModule() || it.isHonor() || it.isLiveOrder() || it.isUgcSeason() || it.isLikeComment()\n        }\n\n        if (isUnite) {\n            if (blockViewPageAds) {\n                viewReply.callMethod(\"clearCm\")\n            }\n\n            if (hidden && removeUpVipLabel) {\n                viewReply.callMethod(\"getOwner\")?.callMethod(\"getVip\")?.callMethod(\"clearLabel\")\n            }\n\n            viewReply.callMethod(\"getTab\")?.run {\n                callMethod(\"ensureTabModuleIsMutable\")\n                val tabModuleList = callMethodAs<MutableList<Any>>(\"getTabModuleList\")\n                if (blockVideoComment) {\n                    tabModuleList.removeAll {\n                        it.callMethodAs(\"hasReply\")\n                    }\n                }\n                if (!(blockCommentGuide || blockViewPageAds || removeHonor || removeUgcSeason)) return@run\n                tabModuleList.firstOrNull { it.callMethod(\"hasIntroduction\") == true }?.let {\n                    it.callMethodAs<Any>(\"getIntroduction\").run {\n                        callMethod(\"ensureModulesIsMutable\")\n                        callMethodAs<MutableList<Any>>(\"getModulesList\").filter()\n                    }\n                }\n                if (blockCommentGuide) {\n                    tabModuleList.firstOrNull { it.callMethod(\"hasReply\") == true }?.let { tabModule ->\n                        tabModule.callMethod(\"getReply\")?.callMethod(\"getReplyStyle\")\n                            ?.callMethod(\"clearBadgeType\")\n                    }\n                }\n            }\n\n            return\n        }\n\n        if (unlockPlayLimit)\n            viewReply.callMethod(\"getConfig\")?.callMethod(\"setShowListenButton\", true)\n        if (blockCommentGuide) {\n            viewReply.runCatchingOrNull {\n                callMethod(\"getLikeCustom\")\n                    ?.callMethod(\"clearLikeComment\")\n                callMethod(\"getReplyPreface\")\n                    ?.callMethod(\"clearBadgeType\")\n            }\n        }\n\n        if (hidden && removeHonor) {\n            viewReply.callMethod(\"clearHonor\")\n        }\n        if (hidden && removeUgcSeason) {\n            viewReply.callMethod(\"clearUgcSeason\")\n        }\n        if (hidden && blockLiveOrder) {\n            viewReply.callMethod(\"clearLiveOrderInfo\")\n        }\n        if (hidden && removeUpVipLabel) {\n            viewReply.callMethod(\"getOwnerExt\")?.callMethod(\"getVip\")?.callMethod(\"clearLabel\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/PublishToFollowingHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.hookBeforeConstructor\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass PublishToFollowingHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"disable_auto_select\", false))\n            return\n        instance.publishToFollowingConfigClass?.hookBeforeConstructor(\n            Boolean::class.javaPrimitiveType,\n            Boolean::class.javaPrimitiveType,\n            Boolean::class.javaPrimitiveType,\n            Boolean::class.javaPrimitiveType,\n        ) { it.args[2]/*autoSelectOnce*/ = false }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/QualityHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass QualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        sPrefs.getString(\"cn_server_accessKey\", null) ?: return\n        Log.d(\"startHook: Quality\")\n\n        \"com.bilibili.lib.accountinfo.model.VipUserInfo\".hookBeforeMethod(\n            mClassLoader,\n            \"isEffectiveVip\"\n        ) {\n            Thread.currentThread().stackTrace.find { stack ->\n                stack.className.contains(\".quality.\")\n            } ?: return@hookBeforeMethod\n            it.result = true\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/RewardAdHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.os.Bundle\nimport android.widget.TextView\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass RewardAdHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n\n        if (!sPrefs.getBoolean(\"skip_reward_ad\", false)) return\n\n        Log.d(\"startHook: RewardAd\")\n\n        instance.rewardAdClass?.hookAfterMethod(\"onCreate\", Bundle::class.java) { params ->\n            params.thisObject.setBooleanField(instance.rewardFlag(), true)\n            (params.thisObject.javaClass.declaredFields.firstOrNull {\n                it.type == TextView::class.java\n            }?.apply { isAccessible = true }?.get(params.thisObject) as? TextView)?.performClick()\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/SSLHook.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage me.iacn.biliroaming.hook\n\nimport android.annotation.SuppressLint\nimport android.net.http.SslError\nimport android.webkit.SslErrorHandler\nimport android.webkit.WebView\nimport me.iacn.biliroaming.utils.*\nimport org.apache.http.conn.scheme.HostNameResolver\nimport org.apache.http.conn.ssl.SSLSocketFactory\nimport java.net.Socket\nimport java.security.KeyStore\nimport java.security.SecureRandom\nimport java.security.cert.X509Certificate\nimport javax.net.ssl.*\n\nclass SSLHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        Log.d(\"startHook: Ssl\")\n\n        val emptyTrustManagers = arrayOf(object : X509TrustManager {\n            @SuppressLint(\"TrustAllX509TrustManager\")\n            override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {\n            }\n\n            @SuppressLint(\"TrustAllX509TrustManager\")\n            override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {\n            }\n\n            override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()\n\n            @Suppress(\"unused\", \"UNUSED_PARAMETER\")\n            fun checkServerTrusted(\n                chain: Array<X509Certificate>,\n                authType: String,\n                host: String\n            ): List<X509Certificate> = emptyList()\n        })\n\n        \"javax.net.ssl.TrustManagerFactory\".hookBeforeMethod(\n            mClassLoader,\n            \"getTrustManagers\"\n        ) { param ->\n            param.result = emptyTrustManagers\n        }\n\n        \"javax.net.ssl.SSLContext\".hookBeforeMethod(\n            mClassLoader,\n            \"init\",\n            \"javax.net.ssl.KeyManager[]\",\n            \"javax.net.ssl.TrustManager[]\",\n            SecureRandom::class.java\n        ) { param ->\n            param.args[0] = null\n            param.args[1] = emptyTrustManagers\n            param.args[2] = null\n        }\n\n        \"javax.net.ssl.HttpsURLConnection\".hookBeforeMethod(\n            mClassLoader,\n            \"setSSLSocketFactory\",\n            javax.net.ssl.SSLSocketFactory::class.java\n        ) { param ->\n            param.args[0] = \"javax.net.ssl.SSLSocketFactory\".findClass(mClassLoader).new()\n        }\n\n        \"org.apache.http.conn.scheme.SchemeRegistry\".findClassOrNull(mClassLoader)\n            ?.hookBeforeMethod(\"register\", \"org.apache.http.conn.scheme.Scheme\") { param ->\n                if (param.args[0].callMethodAs<String>(\"getName\") == \"https\") {\n                    param.args[0] = param.args[0].javaClass.new(\n                        \"https\",\n                        SSLSocketFactory.getSocketFactory(),\n                        443\n                    )\n                }\n            }\n\n        \"org.apache.http.conn.ssl.HttpsURLConnection\".findClassOrNull(mClassLoader)?.run {\n            hookBeforeMethod(\"setDefaultHostnameVerifier\", HostnameVerifier::class.java) { param ->\n                param.args[0] = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER\n            }\n\n            hookBeforeMethod(\"setHostnameVerifier\", HostnameVerifier::class.java) { param ->\n                param.args[0] = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER\n            }\n\n        }\n        \"org.apache.http.conn.ssl.SSLSocketFactory\".hookBeforeMethod(\n            mClassLoader,\n            \"getSocketFactory\"\n        ) { param ->\n            param.result = SSLSocketFactory::class.java.new()\n        }\n\n        \"org.apache.http.conn.ssl.SSLSocketFactory\".findClassOrNull(mClassLoader)\n            ?.hookAfterConstructor(\n                String::class.java,\n                KeyStore::class.java,\n                String::class.java,\n                KeyStore::class.java,\n                SecureRandom::class.java,\n                HostNameResolver::class.java\n            ) { param ->\n                val algorithm = param.args[0] as? String\n                val keystore = param.args[1] as? KeyStore\n                val keystorePassword = param.args[2] as? String\n                val random = param.args[4] as? SecureRandom\n\n                @Suppress(\"UNCHECKED_CAST\") val trustManagers =\n                    emptyTrustManagers as Array<TrustManager>\n\n                val keyManagers = keystore?.let {\n                    SSLSocketFactory::class.java.callStaticMethodAs<Array<KeyManager>>(\n                        \"createKeyManagers\",\n                        keystore,\n                        keystorePassword\n                    )\n                }\n\n\n                param.thisObject.setObjectField(\"sslcontext\", SSLContext.getInstance(algorithm))\n                param.thisObject.getObjectField(\"sslcontext\")\n                    ?.callMethod(\"init\", keyManagers, trustManagers, random)\n                param.thisObject.setObjectField(\n                    \"socketfactory\",\n                    param.thisObject.getObjectField(\"sslcontext\")?.callMethod(\"getSocketFactory\")\n                )\n            }\n\n        \"org.apache.http.conn.ssl.SSLSocketFactory\".hookAfterMethod(\n            mClassLoader,\n            \"isSecure\",\n            Socket::class.java\n        ) { param ->\n            param.result = true\n        }\n\n        \"okhttp3.CertificatePinner\".findClassOrNull(mClassLoader)?.run {\n            (runCatchingOrNull { getDeclaredMethod(\"findMatchingPins\", String::class.java) }\n                ?: declaredMethods.firstOrNull { it.parameterTypes.size == 1 && it.parameterTypes[0] == String::class.java && it.returnType == List::class.java })?.hookBeforeMethod { param ->\n                param.args[0] = \"\"\n            }\n        }\n\n        \"android.webkit.WebViewClient\".findClassOrNull(mClassLoader)?.run {\n            replaceMethod(\n                \"onReceivedSslError\",\n                WebView::class.java,\n                SslErrorHandler::class.java,\n                SslError::class.java\n            ) { param ->\n                (param.args[1] as SslErrorHandler).proceed()\n                null\n            }\n            replaceMethod(\n                \"onReceivedError\",\n                WebView::class.java,\n                Int::class.javaPrimitiveType,\n                String::class.java,\n                String::class.java\n            ) {\n                null\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/SettingHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.SettingDialog\nimport me.iacn.biliroaming.utils.*\nimport java.lang.reflect.Constructor\nimport java.lang.reflect.Proxy\n\n\nclass SettingHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    private var startSetting = false\n\n    override fun startHook() {\n        Log.d(\"startHook: Setting\")\n\n        instance.splashActivityClass?.hookBeforeMethod(\"onCreate\", Bundle::class.java) { param ->\n            val self = param.thisObject as Activity\n            startSetting = self.intent.hasExtra(START_SETTING_KEY)\n        }\n\n        instance.mainActivityClass?.hookAfterMethod(\"onResume\") { param ->\n            if (startSetting) {\n                startSetting = false\n                SettingDialog.show(param.thisObject as Activity)\n            }\n        }\n\n        instance.mainActivityClass?.hookBeforeMethod(\n            \"onCreate\",\n            Bundle::class.java\n        ) { param ->\n            val bundle = param.args[0] as? Bundle\n            bundle?.remove(\"android:fragments\")\n        }\n\n        instance.drawerClass?.hookAfterMethod(\n            \"onCreateView\",\n            LayoutInflater::class.java,\n            ViewGroup::class.java,\n            Bundle::class.java\n        ) { param ->\n            val navSettingId = getId(\"nav_settings\")\n            val nav =\n                param.thisObject.javaClass.declaredFields.first { it.type.name == \"android.support.design.widget.NavigationView\" }.name\n            (param.thisObject.getObjectField(nav)\n                ?: param.result).callMethodAs<View>(\"findViewById\", navSettingId)\n                .setOnLongClickListener {\n                    SettingDialog.show(param.thisObject.callMethodAs<Activity>(\"getActivity\"))\n                    true\n                }\n        }\n\n        instance.homeCenters().forEach { (c, m) ->\n            c?.hookBeforeAllMethods(m) { param ->\n                @Suppress(\"UNCHECKED_CAST\")\n                val list = param.args[1] as? MutableList<Any>\n                    ?: param.args[1]?.getObjectFieldOrNullAs<MutableList<Any>>(\"moreSectionList\")\n                    ?: return@hookBeforeAllMethods\n\n                val itemList = list.lastOrNull()?.let {\n                    if (it.javaClass != instance.menuGroupItemClass) it.getObjectFieldOrNullAs<MutableList<Any>>(\n                        \"itemList\"\n                    ) else list\n                } ?: list\n\n                val item = instance.menuGroupItemClass?.new() ?: return@hookBeforeAllMethods\n                item.setIntField(\"id\", SETTING_ID)\n                    .setObjectField(\"title\", \"哔哩漫游设置\")\n                    .setObjectField(\n                        \"icon\",\n                        \"https://i0.hdslb.com/bfs/album/276769577d2a5db1d9f914364abad7c5253086f6.png\"\n                    )\n                    .setObjectField(\"uri\", SETTING_URI)\n                    .setIntField(\"visible\", 1)\n                itemList.forEach {\n                    if (try {\n                            it.getIntField(\"id\") == SETTING_ID\n                        } catch (t: Throwable) {\n                            it.getLongField(\"id\") == SETTING_ID.toLong()\n                        }\n                    ) return@hookBeforeAllMethods\n                }\n                itemList.add(item)\n            }\n        }\n\n        instance.settingRouterClass?.hookBeforeAllConstructors { param ->\n            if (param.args[1] != SETTING_URI) return@hookBeforeAllConstructors\n            val routerType = (param.method as Constructor<*>).parameterTypes[3]\n            param.args[3] = Proxy.newProxyInstance(\n                routerType.classLoader,\n                arrayOf(routerType)\n            ) { _, method, _ ->\n                val returnType = method.returnType\n                Proxy.newProxyInstance(\n                    returnType.classLoader,\n                    arrayOf(returnType)\n                ) { _, method2, args ->\n                    when (method2.returnType) {\n                        Boolean::class.javaPrimitiveType -> false\n                        else -> {\n                            if (method2.parameterTypes.isNotEmpty() &&\n                                method2.parameterTypes[0].name == \"android.app.Activity\"\n                            ) {\n                                val currentActivity = args[0] as Activity\n                                SettingDialog.show(currentActivity)\n                            }\n                            null\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    companion object {\n        const val START_SETTING_KEY = \"biliroaming_start_setting\"\n        const val SETTING_URI = \"bilibili://biliroaming\"\n        const val SETTING_ID = 114514\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/ShareHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.net.Uri\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.bv2av\nimport me.iacn.biliroaming.utils.getObjectField\nimport me.iacn.biliroaming.utils.hookAfterMethod\nimport me.iacn.biliroaming.utils.sPrefs\nimport me.iacn.biliroaming.utils.setObjectField\nimport java.net.HttpURLConnection\nimport java.net.URL\n\nclass ShareHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    private val contentUrlPattern = Regex(\"\"\"[\\s\\S]*(https?://(?:bili2233\\.cn|b23\\.tv)/\\S*)$\"\"\")\n\n    private fun String.resolveB23URL(): String {\n        val conn = URL(this).openConnection() as HttpURLConnection\n        conn.requestMethod = \"GET\"\n        conn.instanceFollowRedirects = false\n        conn.connect()\n        if (conn.responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {\n            return conn.getHeaderField(\"Location\")\n        }\n        return this\n    }\n\n    private fun transformUrl(url: String, transformAv: Boolean): String {\n        val target = Uri.parse(url)\n        val bv = if (transformAv) {\n            target.path?.split(\"/\")?.firstOrNull { it.startsWith(\"BV\") && it.length == 12 }\n        } else {\n            null\n        }\n        val av = bv?.let { \"av${bv2av(bv)}\" }\n        val newUrl = target.buildUpon()\n        if (av != null) {\n            newUrl.path(target.path!!.replace(bv, av))\n        }\n        val encodedQuery = target.encodedQuery\n        if (encodedQuery != null) {\n            val query = encodedQuery.split(\"&\").map {\n                it.split(\"=\")\n            }.filter {\n                it.size == 2\n            }.mapNotNull {\n                when {\n                    it[0] == \"p\" || it[0] == \"t\" -> \"${it[0]}=${it[1]}\"\n                    it[0] == \"start_progress\" -> \"start_progress=${it[1]}&t=${it[1].toLong() / 1000}\"\n                    else -> null\n                }\n            }.joinToString(\"&\", postfix = \"&unique_k=2333\")\n            newUrl.encodedQuery(query)\n        } else {\n            newUrl.appendQueryParameter(\"unique_k\", \"2333\")\n        }\n        return newUrl.build().toString()\n    }\n\n    override fun startHook() {\n        val miniProgramEnabled = sPrefs.getBoolean(\"mini_program\", false)\n        val purifyShareEnabled = sPrefs.getBoolean(\"purify_share\", false)\n        if (!miniProgramEnabled && !purifyShareEnabled) return\n        Log.d(\"startHook: ShareHook\")\n        instance.shareClickResultClass?.apply {\n            if (purifyShareEnabled) {\n                hookAfterMethod(\"getLink\") { param ->\n                    (param.result as? String)?.takeIf {\n                        it.startsWith(\"https://bili2233.cn\") || it.startsWith(\"http://bili2233.cn\") || it.startsWith(\"https://b23.tv\") || it.startsWith(\"http://b23.tv\")\n                    }?.let {\n                        val targetUrl = Uri.parse(it).buildUpon().query(\"\").build().toString()\n                        param.result = targetUrl.resolveB23URL().also { r -> param.thisObject.setObjectField(\"link\", r) }\n                    }\n                }\n                hookAfterMethod(\"getContent\") { param ->\n                    val content = param.result as? String\n                    content?.let {\n                        contentUrlPattern.matchEntire(it)?.groups?.get(1)?.value\n                    }?.let { contentUrl ->\n                        val resolvedUrl = (param.thisObject.getObjectField(\"link\")?.let { it as String } ?: contentUrl)\n                            .let {\n                                if (it.startsWith(\"https://bili2233.cn\") || it.startsWith(\"http://bili2233.cn\") || it.startsWith(\"https://b23.tv\") || it.startsWith(\"http://b23.tv\"))\n                                    it.resolveB23URL()\n                                else it\n                            }\n                        param.result = content.replace(contentUrl, transformUrl(resolvedUrl, miniProgramEnabled)).also { r ->\n                            param.thisObject.setObjectField(\"content\", r)\n                        }\n                    }\n                }\n            }\n            if (!miniProgramEnabled) return@apply\n            // ShareMode Definition\n            // 1: PARAMS_TYPE_TEXT\n            // 2: PARAMS_TYPE_AUDIO\n            // 4: PARAMS_TYPE_VIDEO\n            // 5: PARAMS_TYPE_IMAGE\n            // 6 / 7: PARAMS_TYPE_MIN_PROGRAM\n            // 21: PARAMS_TYPE_PURE_IMAGE\n            // Others: PARAMS_TYPE_WEB\n            hookAfterMethod(\"getShareMode\") { param ->\n                if (param.result == 6 || param.result == 7) {\n                    param.result = 0\n                    param.thisObject.apply {\n                        getObjectField(\"title\")?.takeIf { it == \"哔哩哔哩\" }?.let { title ->\n                            setObjectField(\"title\", getObjectField(\"content\"))\n                            setObjectField(\"content\", \"由哔哩漫游分享\")\n                        }\n                        getObjectField(\"content\")?.let { it as String }\n                            ?.takeIf { it.startsWith(\"已观看\") }?.let { content ->\n                                setObjectField(\"content\", \"$content\\n由哔哩漫游分享\")\n                            }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/SpeedHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.callMethod\nimport me.iacn.biliroaming.utils.callMethodOrNullAs\nimport me.iacn.biliroaming.utils.getObjectField\nimport me.iacn.biliroaming.utils.hookAfterAllConstructors\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass SpeedHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        Log.d(\"startHook: SpeedHook\")\n        val defaultPlaybackSpeed = sPrefs.getInt(\"default_speed\", 100) / 100f\n        if (defaultPlaybackSpeed == 1f) return\n        instance.playSpeedManager?.hookAfterAllConstructors {\n            for (f in it.thisObject.javaClass.declaredFields) {\n                val o = it.thisObject.getObjectField(f.name)\n                val v = o?.callMethodOrNullAs<Float?>(\"getValue\") ?: continue\n                if (v != 1f) continue\n                o.callMethod(\"setValue\", defaultPlaybackSpeed)\n                Log.toast(\"已设置 $defaultPlaybackSpeed 倍速\")\n                break\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/SplashHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.content.res.Configuration\nimport android.graphics.Color\nimport android.net.Uri\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.ImageView\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\nimport java.io.File\n\nclass SplashHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"custom_splash\", false) && !sPrefs.getBoolean(\n                \"custom_splash_logo\",\n                false\n            )\n            && !sPrefs.getBoolean(\"full_splash\", false) && !sPrefs.getBoolean(\"auto_dark_splash\", false)\n        ) return\n        Log.d(\"startHook: Splash\")\n\n        instance.splashInfoClass?.hookAfterMethod(\n            \"getMode\"\n        ) { param ->\n            param.result = if (sPrefs.getBoolean(\"full_splash\", false)) {\n                \"full\"\n            } else {\n                param.result\n            }\n        }\n\n        instance.brandSplashClass?.hookAfterMethod(\n            \"onViewCreated\",\n            View::class.java,\n            Bundle::class.java\n        ) { param ->\n            val view = param.args[0] as View\n            val containerId = getId(\"splash_container\")\n            if (sPrefs.getBoolean(\"auto_dark_splash\", false))\n                view.findViewById<View>(containerId)\n                    .setBackgroundColor(\n                        if (view.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES)\n                            Color.BLACK\n                        else Color.WHITE\n                    )\n            if (sPrefs.getBoolean(\"custom_splash\", false)) {\n                val brandId = getId(\"brand_splash\")\n                val fullId = getId(\"full_brand_splash\")\n                val brandSplash = view.findViewById<ImageView>(brandId)\n                val full = if (fullId != 0) view.findViewById<ImageView>(fullId) else null\n                val splashImage = File(currentContext.filesDir, SPLASH_IMAGE)\n                if (splashImage.exists()) {\n                    val uri = Uri.fromFile(splashImage)\n                    brandSplash.setImageURI(uri)\n                    full?.setImageURI(uri)\n                } else {\n                    brandSplash.alpha = .0f\n                    full?.alpha = .0f\n                }\n            }\n            if (sPrefs.getBoolean(\"custom_splash_logo\", false)) {\n                val logoId = getId(\"brand_logo\")\n                val brandLogo = view.findViewById<ImageView>(logoId)\n                val logoImage = File(currentContext.filesDir, LOGO_IMAGE)\n                if (logoImage.exists())\n                    brandLogo.setImageURI(Uri.fromFile(logoImage))\n                else\n                    brandLogo.alpha = .0f\n            }\n        }\n    }\n\n    companion object {\n        const val SPLASH_IMAGE = \"biliroaming_splash\"\n        const val LOGO_IMAGE = \"biliroaming_logo\"\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/StartActivityHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport android.app.Instrumentation\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport me.iacn.biliroaming.BiliBiliPackage\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.hookBeforeAllMethods\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.packageName\nimport me.iacn.biliroaming.utils.sPrefs\nimport me.iacn.biliroaming.utils.toJSONObject\nimport kotlin.math.floor\n\nclass StartActivityHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n\n    private fun fixIntentUri(original: Uri): Uri {\n        val fixedUri = Uri.parse(original.toString().replace(\"bilibili://story/\", \"bilibili://united_video/\")).buildUpon()\n            .clearQuery()\n            .appendQueryParameter(\"from_spmid\", original.getQueryParameter(\"from_spmid\"))\n            .appendQueryParameter(\"aid\", original.path?.split(\"/\")?.last() ?: \"\")\n            .appendQueryParameter(\"bvid\", \"\")\n            .build()\n        return fixedUri\n    }\n\n    override fun startHook() {\n        \"tv.danmaku.bili.ui.intent.IntentHandlerActivity\".hookBeforeMethod(mClassLoader, \"onCreate\", Bundle::class.java) { param ->\n            val a = param.thisObject as Activity\n            val data = a.intent.data ?: return@hookBeforeMethod\n            a.intent.data = data.buildUpon().encodedQuery(data.encodedQuery?.replace(\"&-Arouter=story\", \"\")?.replace(\"&-Atype=story\", \"\")).build()\n        }\n        Instrumentation::class.java.hookBeforeAllMethods(\"execStartActivity\") { param ->\n            val intent = param.args[4] as? Intent ?: return@hookBeforeAllMethods\n            val uri = intent.dataString ?: return@hookBeforeAllMethods\n            if (sPrefs.getBoolean(\n                    \"replace_story_video\",\n                    false\n                ) && uri.startsWith(\"bilibili://story/\")\n            ) {\n                if (instance.hasUnitedVideoActivity) {\n                    intent.data?.let {\n                        try {\n                            val cid = intent.data?.getQueryParameter(\"player_preload\").toJSONObject().getLong(\"cid\")\n                            intent.data = fixIntentUri(Uri.parse(intent.dataString))\n                            // fix extra\n                            val pre = Uri.parse(intent.dataString).buildUpon().clearQuery().build().toString()\n                            val aid = pre.split(\"/\").last().toLong()\n                            intent.removeExtra(\"player_preload\")\n                            intent.putExtra(\"player_preload\", floor(Math.random()*1000000000).toInt().toString())\n                            intent.putExtra(\"blrouter.targeturl\", pre)\n                            intent.putExtra(\"blrouter.pagename\", \"bilibili://united_video/\")\n                            intent.putExtra(\"jumpFrom\", 7)\n                            intent.putExtra(\"\", aid)\n                            intent.putExtra(\"aid\", aid)\n                            intent.putExtra(\"cid\", cid)\n                            intent.putExtra(\"bvid\", \"\")\n                            intent.putExtra(\"from\", 7)\n                            intent.putExtra(\"blrouter.targeturl\", pre)\n                            intent.putExtra(\"blrouter.matchrule\", \"bilibili://united_video/\")\n                            // fix component\n                            intent.component = ComponentName(\n                                intent.component?.packageName ?: packageName,\n                                \"com.bilibili.ship.theseus.detail.UnitedBizDetailsActivity\"\n                            )\n                        } catch (e: Exception) {\n                            Log.e(\"replaceStoryVideo fix intent failed!!!\")\n                            Log.e(e)\n                        }\n                    }\n                    return@hookBeforeAllMethods\n                }\n                // 兼容旧版\n                intent.component = ComponentName(\n                    intent.component?.packageName ?: packageName,\n                    \"com.bilibili.video.videodetail.VideoDetailsActivity\"\n                )\n                intent.data = Uri.parse(uri.replace(\"bilibili://story/\", \"bilibili://video/\"))\n            }\n            if (sPrefs.getBoolean(\"force_browser\", false)) {\n                if (intent.component?.className?.endsWith(\"MWebActivity\") == true &&\n                        intent.data?.authority?.matches(whileListDomain) == false) {\n                    Log.d(\"force_browser ${intent.data?.authority}\")\n                    param.args[4] = Intent(Intent.ACTION_VIEW).apply {\n                        data = intent.data\n                    }\n                }\n            }\n        }\n    }\n    companion object {\n        val whileListDomain = Regex(\"\"\".*bilibili\\.com|.*b23\\.tv\"\"\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/StoryPlayerAdHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.from\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass StoryPlayerAdHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n\n\n        val purifyTags = sPrefs.getStringSet(\"purify_story_video_ad_tags\", emptySet()) ?: emptySet()\n        if (purifyTags.isEmpty()) return\n\n        Log.d(\"startHook: StoryPlayerAd, purifyTags: $purifyTags\")\n\n        instance.storyPagerPlayerClass?.hookBeforeMethod(\n            instance.addVideo(), List::class.java) { params ->\n                val storyDetailList = params.args[0] as? MutableList<*> ?: return@hookBeforeMethod\n                val toRemove = mutableListOf<Any>()\n\n                storyDetailList.forEach {\n                    val storyDetail = it!!::class.java\n                    val getCartIconInfoMethod = storyDetail.getDeclaredMethod(\"getCartIconInfo\")\n                    val isAdMethod = storyDetail.getDeclaredMethod(\"isAd\")\n                    getCartIconInfoMethod.isAccessible = true\n                    isAdMethod.isAccessible = true\n\n                    val isAd = isAdMethod.invoke(it) as? Boolean\n\n                    var cartInfoText: String? = null\n                    val cartIconInfo = getCartIconInfoMethod.invoke(it)\n                    if (cartIconInfo != null) {\n                        val cartClass = cartIconInfo.javaClass\n                        val getEntryTextMethod = cartClass.getDeclaredMethod(\"getEntryText\")\n                        getEntryTextMethod.isAccessible = true\n                        cartInfoText = getEntryTextMethod.invoke(cartIconInfo) as? String\n                    }\n\n                    val shouldRemove = when {\n                        \"ad\" in purifyTags && isAd == true -> true\n                        \"short\" in purifyTags && cartInfoText == \"短剧\" -> true\n                        \"shopping\" in purifyTags && cartInfoText == \"购物\" -> true\n                        \"tv\" in purifyTags && cartInfoText == \"电视剧\" -> true\n                        \"doc\" in purifyTags && cartInfoText == \"纪录片\" -> true\n                        \"ent\" in purifyTags && cartInfoText == \"娱乐\" -> true\n                        \"movie\" in purifyTags && cartInfoText == \"电影\" -> true\n                        \"music\" in purifyTags && cartInfoText == \"音乐\" -> true\n                        \"topic\" in purifyTags && cartInfoText == \"话题\" -> true\n                        else -> false\n                    }\n                    if (shouldRemove) {\n                        toRemove.add(it)\n                    }\n                }\n\n                storyDetailList.removeAll(toRemove.toSet())\n                val blockedCount = sPrefs.getInt(\"purify_story_video_ad_blocked_count\", 0)\n                sPrefs.edit().putInt(\"purify_story_video_ad_blocked_count\", blockedCount + toRemove.size).apply()\n            }\n\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/SubtitleHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.graphics.*\nimport android.net.Uri\nimport android.text.Layout\nimport android.text.SpannableString\nimport android.text.StaticLayout\nimport android.text.TextPaint\nimport android.text.style.AbsoluteSizeSpan\nimport android.text.style.LineBackgroundSpan\nimport android.text.style.MaskFilterSpan\nimport android.text.style.StyleSpan\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.launch\nimport me.iacn.biliroaming.*\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.hook.BangumiSeasonHook.Companion.lastSeasonInfo\nimport me.iacn.biliroaming.hook.ProtoBufHook.Companion.removeCmdDms\nimport me.iacn.biliroaming.network.BiliRoamingApi\nimport me.iacn.biliroaming.utils.*\nimport org.json.JSONArray\nimport java.io.File\nimport java.lang.reflect.Method\nimport java.lang.reflect.Proxy\nimport kotlin.math.ceil\nimport kotlin.math.roundToInt\n\n\nclass SubtitleHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    companion object {\n        val fontFile by lazy { File(currentContext.getExternalFilesDir(null), \"subtitle.font\") }\n\n        val backgroundSpan = { backgroundColor: Int, textSize: Int ->\n            LineBackgroundSpan { canvas, paint, left, right, top, _, bottom, text, start, end, _ ->\n                val ts = paint.textSize\n                paint.textSize = textSize.toFloat()\n                val width = paint.measureText(text, start, end).roundToInt()\n                val textLeft = (right + left - width) / 2\n                val color = paint.color\n                val rect = Rect()\n                rect.set(textLeft, top, textLeft + width, bottom)\n                paint.color = backgroundColor\n                canvas.drawRect(rect, paint)\n                paint.textSize = ts\n                paint.color = color\n            }\n        }\n\n        fun subtitleStylizeRunner(\n            subtitle: SpannableString,\n            start: Int,\n            end: Int,\n            flags: Int,\n            blurSolid: Int,\n            fontColor: String,\n            fontSize: Int,\n            bgColor: String,\n            strokeColor: String,\n            strokeWidth: Float,\n            fixBreak: Boolean\n        ) {\n            val subtitleBlurSolid = blurSolid.toString() + \"f\"\n            val fc = Color.parseColor(\"#$fontColor\")\n            val sc = Color.parseColor(\"#$strokeColor\")\n            if (fixBreak)\n                (start until end).forEach { i ->\n                    subtitle.setSpan(\n                        StrokeSpan(fc, sc, strokeWidth),\n                        i,\n                        i + 1,\n                        flags\n                    )\n                }\n            else\n                subtitle.setSpan(StrokeSpan(fc, sc, strokeWidth), start, end, flags)\n            subtitle.setSpan(\n                AbsoluteSizeSpan(fontSize, false),\n                start,\n                end,\n                flags\n            )\n            subtitle.setSpan(\n                StyleSpan(Typeface.BOLD),\n                start,\n                end,\n                flags\n            )\n            if (blurSolid != 0) {\n                subtitle.setSpan(\n                    MaskFilterSpan(\n                        BlurMaskFilter(\n                            subtitleBlurSolid.toFloat(),\n                            BlurMaskFilter.Blur.SOLID\n                        )\n                    ),\n                    start,\n                    end,\n                    flags\n                )\n            }\n            // should be drawn the last\n            subtitle.setSpan(\n                backgroundSpan(Color.parseColor(\"#$bgColor\"), fontSize),\n                start,\n                end,\n                flags\n            )\n        }\n    }\n\n    private val hidden = sPrefs.getBoolean(\"hidden\", false)\n    private val removeCmdDms = sPrefs.getBoolean(\"remove_video_cmd_dms\", false)\n\n    private val mainFunc by lazy { sPrefs.getBoolean(\"main_func\", false) }\n    private val generateSubtitle by lazy { sPrefs.getBoolean(\"auto_generate_subtitle\", false) }\n    private val addCloseSubtitle by lazy { mainFunc && getVersionCode(packageName) >= 6750300 }\n    private val customSubtitle by lazy { sPrefs.getBoolean(\"custom_subtitle\", false) }\n    private val removeBg by lazy { sPrefs.getBoolean(\"subtitle_remove_bg\", true) }\n    private val boldText by lazy { sPrefs.getBoolean(\"subtitle_bold\", true) }\n    private val fontSizePortrait by lazy { sPrefs.getInt(\"subtitle_font_size_portrait\", 0).sp }\n    private val fontSizeLandscape by lazy { sPrefs.getInt(\"subtitle_font_size_landscape\", 0).sp }\n    private val fillColor by lazy {\n        sPrefs.getString(\"subtitle_font_color2\", null)\n            ?.runCatchingOrNull { Color.parseColor(\"#$this\") } ?: Color.WHITE\n    }\n    private val strokeColor by lazy {\n        sPrefs.getString(\"subtitle_stroke_color\", null)\n            ?.runCatchingOrNull { Color.parseColor(\"#$this\") } ?: Color.BLACK\n    }\n    private val strokeWidth by lazy {\n        sPrefs.getFloat(\"subtitle_stroke_width\", 5.0F)\n    }\n    private val offset by lazy { sPrefs.getInt(\"subtitle_offset\", 0) }\n\n    private val closeText by lazy {\n        currentContext.getString(getResId(\"Player_option_subtitle_lan_doc_nodisplay\", \"string\"))\n    }\n\n    private var subtitleFont: Typeface? = null\n\n    override fun startHook() {\n        if (customSubtitle)\n            if (instance.cronCanvasClass == null) {\n                hookSubtitleStyle()\n            } else {\n                hookSubtitleStyleNew()\n            }\n        if (mainFunc || generateSubtitle)\n            hookSubtitleList()\n\n        hookDmViewAsync()\n    }\n\n    private fun hookDmViewAsync() {\n        if (!(mainFunc || generateSubtitle || (hidden && removeCmdDms))) return\n        instance.dmMossClass?.hookBeforeMethod(\n            \"dmView\",\n            instance.dmViewReqClass,\n            instance.mossResponseHandlerClass\n        ) { param ->\n            val dmViewReq = param.args[0]\n            param.args[1] = param.args[1].mossResponseHandlerReplaceProxy { dmViewReply ->\n                if (hidden && removeCmdDms) {\n                    dmViewReply?.removeCmdDms()\n                }\n                if (mainFunc || generateSubtitle) {\n                    dmViewReply.hookSubtitleList(dmViewReq)\n                } else null\n            }\n        }\n    }\n\n    private fun hookSubtitleStyle() {\n        instance.chronosSwitchClass?.hookAfterConstructor { param ->\n            param.thisObject.javaClass.declaredFields.forEach {\n                if (it.type == Boolean::class.javaObjectType) {\n                    param.thisObject.setObjectField(it.name, false)\n                }\n            }\n        }\n\n        android.text.SpannableString::class.java.hookBeforeMethod(\n            \"setSpan\",\n            Object::class.java,\n            Int::class.java,\n            Int::class.java,\n            Int::class.java\n        ) { param ->\n            if (instance.subtitleSpanClass?.isInstance(param.args[0]) != true) return@hookBeforeMethod\n            val (start, end, flags) = listOf(\n                param.args[1] as Int,\n                param.args[2] as Int,\n                param.args[3] as Int\n            )\n            (param.thisObject as SpannableString).run {\n                subtitleStylizeRunner(\n                    this, start, end, flags,\n                    sPrefs.getInt(\"subtitle_blur_solid\", 1),\n                    sPrefs.getString(\n                        \"subtitle_font_color2\",\n                        \"FFFFFFFF\"\n                    )!!,\n                    sPrefs.getInt(\"subtitle_font_size\", 30),\n                    sPrefs.getString(\n                        \"subtitle_background_color\",\n                        \"20000000\"\n                    )!!,\n                    sPrefs.getString(\"subtitle_stroke_color\", \"00000000\")!!,\n                    sPrefs.getFloat(\"subtitle_stroke_width\", 0F),\n                    sPrefs.getBoolean(\"subtitle_fix_break\", false)\n                )\n                param.result = null\n            }\n        }\n    }\n\n    private fun hookSubtitleStyleNew() {\n        if (offset != 0) {\n            arrayOf(instance.subtitleConfigGetClass, instance.subtitleConfigChangeClass).forEach {\n                it?.hookBeforeMethod(\"setBottomMargin\", Float::class.javaObjectType) { param ->\n                    param.args[0] = (param.args[0] as Float) + offset\n                }\n            }\n        }\n        val cronCanvasClass = instance.cronCanvasClass ?: return\n        if (removeBg) {\n            cronCanvasClass.replaceMethod(\n                \"drawPath\",\n                Path::class.java,\n                Boolean::class.javaPrimitiveType\n            ) { null }\n        }\n        val paintField = cronCanvasClass.getDeclaredField(\"paint\")\n            .apply { isAccessible = true }\n        val fillColorField = cronCanvasClass.getDeclaredField(\"fillColor\")\n            .apply { isAccessible = true }\n        val strokeColorField = cronCanvasClass.getDeclaredField(\"strokeColor\")\n            .apply { isAccessible = true }\n        val maxWidthField = cronCanvasClass.getDeclaredField(\"maxWidth\")\n            .apply { isAccessible = true }\n        val staticLayoutField = cronCanvasClass.getDeclaredField(\"staticLayout\")\n            .apply { isAccessible = true }\n        val alignmentField = cronCanvasClass.getDeclaredField(\"alignment\")\n            .apply { isAccessible = true }\n        val measureTextFromLayoutMethod = cronCanvasClass.getDeclaredMethod(\n            \"measureTextFromLayout\", StaticLayout::class.java\n        ).apply { isAccessible = true }\n        MainScope().launch(Dispatchers.IO) {\n            subtitleFont = if (fontFile.isFile) {\n                Typeface.createFromFile(fontFile)\n            } else null\n        }\n        cronCanvasClass.hookBeforeMethod(\n            \"measureTextImpl\",\n            String::class.java\n        ) { param ->\n            val cronCanvas = param.thisObject\n            val maxWidth = maxWidthField.getFloat(cronCanvas)\n            if (maxWidth != 0.0F) {\n                val paint = paintField.get(cronCanvas) as TextPaint\n                paint.strokeWidth = strokeWidth\n                paint.isFakeBoldText = boldText\n                subtitleFont?.let { paint.typeface = it }\n                if (currentIsLandscape && fontSizeLandscape > 0) {\n                    paint.textSize = fontSizeLandscape.toFloat()\n                } else if (!currentIsLandscape && fontSizePortrait > 0) {\n                    paint.textSize = fontSizePortrait.toFloat()\n                }\n                val text = param.args[0] as String\n                val alignment = alignmentField.get(cronCanvas) as Layout.Alignment\n                val staticLayout = staticLayoutField.get(cronCanvas) as? StaticLayout\n                if (staticLayout != null && staticLayout.text == text) {\n                    param.result = measureTextFromLayoutMethod(null, staticLayout)\n                } else {\n                    val wantWidth = if (maxWidth <= 0.0F) Int.MAX_VALUE\n                    else maxWidth.toInt().coerceAtMost(Int.MAX_VALUE)\n                    var layout = StaticLayout.Builder\n                        .obtain(text, 0, text.length, paint, wantWidth)\n                        .setAlignment(alignment)\n                        .setIncludePad(false)\n                        .build()\n                    val lineMaxWidth = IntRange(0, layout.lineCount - 1).maxOfOrNull {\n                        ceil(layout.getLineWidth(it)).toInt()\n                    } ?: 0\n                    layout = StaticLayout.Builder\n                        .obtain(text, 0, text.length, paint, lineMaxWidth)\n                        .setAlignment(alignment)\n                        .setIncludePad(false)\n                        .build()\n                    staticLayoutField.set(cronCanvas, layout)\n                    param.result = measureTextFromLayoutMethod(null, layout)\n                }\n            }\n        }\n        cronCanvasClass.hookBeforeMethod(\n            \"drawText\",\n            String::class.java,\n            Float::class.javaPrimitiveType,\n            Float::class.javaPrimitiveType,\n            Boolean::class.javaPrimitiveType\n        ) { param ->\n            val cronCanvas = param.thisObject\n            val maxWidth = maxWidthField.getFloat(cronCanvas)\n            if (maxWidth != 0.0F) {\n                fillColorField.setInt(cronCanvas, fillColor)\n                strokeColorField.setInt(cronCanvas, strokeColor)\n                param.args[3] = true\n                param.invokeOriginalMethod()\n                param.args[3] = false\n            }\n        }\n    }\n\n    private fun hookSubtitleList() {\n        instance.dmMossClass?.hookAfterMethod(\n            if (instance.useNewMossFunc) \"executeDmView\" else \"dmView\",\n            instance.dmViewReqClass,\n        ) { param ->\n            param.result.hookSubtitleList(param.args[0])?.let { param.result = it }\n        }\n\n        if (!generateSubtitle) return\n        instance.biliCallClass?.hookBeforeMethod(\n            instance.setParser(), instance.parserClass\n        ) { param ->\n            val url = param.thisObject.getObjectField(instance.biliCallRequestField())\n                ?.getObjectField(instance.urlField())?.toString()\n            if (url?.contains(\"zh_converter=t2cn\") != true)\n                return@hookBeforeMethod\n            val parser = param.args[0]\n            param.args[0] = Proxy.newProxyInstance(\n                parser.javaClass.classLoader,\n                arrayOf(instance.parserClass)\n            ) { _, m, args ->\n                val dictReady = if (!SubtitleHelper.dictExist) {\n                    SubtitleHelper.downloadDict()\n                } else true\n                val converted = if (dictReady) {\n                    runCatching {\n                        val responseText = args[0].callMethodAs<String>(instance.string())\n                        SubtitleHelper.convert(responseText)\n                    }.onFailure {\n                        Log.e(it)\n                    }.getOrNull()\n                        ?: SubtitleHelper.errorResponse(XposedInit.moduleRes.getString(R.string.subtitle_convert_failed))\n                } else SubtitleHelper.errorResponse(XposedInit.moduleRes.getString(R.string.subtitle_dict_download_failed))\n\n                val mediaType = instance.mediaTypeClass\n                    ?.callStaticMethod(\n                        instance.get(),\n                        \"application/json; charset=UTF-8\"\n                    ) ?: return@newProxyInstance m(parser, *args)\n                val responseBody = instance.responseBodyClass\n                    ?.callStaticMethod(\n                        instance.create(),\n                        mediaType,\n                        converted\n                    ) ?: return@newProxyInstance m(parser, *args)\n                m(parser, responseBody)\n            }\n        }\n    }\n\n    private fun Any?.hookSubtitleList(originalReq: Any): Any? {\n        val originalReply = this\n        val parseDmViewReply = { r: Any? ->\n            r?.let { DmViewReply.parseFrom(it.callMethodAs<ByteArray>(\"toByteArray\")) }\n        }\n\n        val extraSubtitles = mutableListOf<SubtitleItem>()\n        val oid = originalReq.callMethod(\"getOid\").toString()\n        val tryThailand = lastSeasonInfo.containsKey(oid) && ((\n                lastSeasonInfo.containsKey(\"area\")\n                        && lastSeasonInfo[\"area\"] == \"th\") ||\n                (lastSeasonInfo.containsKey(\"watch_platform\")\n                        && lastSeasonInfo[\"watch_platform\"] == \"1\"\n                        && (originalReply == null || originalReply.callMethod(\"getSubtitle\")\n                    ?.callMethod(\"getSubtitlesCount\") == 0)))\n        if (mainFunc && tryThailand) {\n            val subtitles = if (lastSeasonInfo.containsKey(\"sb$oid\")) {\n                JSONArray(lastSeasonInfo[\"sb$oid\"])\n            } else {\n                val result = BiliRoamingApi.getThailandSubtitles(\n                    lastSeasonInfo[oid] ?: lastSeasonInfo[\"epid\"]\n                )?.toJSONObject()\n                if (result != null && result.optInt(\"code\") == 0) {\n                    result.optJSONObject(\"data\")\n                        ?.optJSONArray(\"subtitles\").orEmpty()\n                } else JSONArray()\n            }\n            if (subtitles.length() != 0) {\n                extraSubtitles += subtitles.toSubtitles()\n            }\n            // Disable danmaku for Area Th\n            originalReply?.run {\n                callMethod(\"setClosed\", true)\n                callMethod(\"setInputPlaceholder\", \"泰区不支持\")\n            }\n        }\n\n        val fromPGC = originalReq.callMethodAs<String>(\"getSpmid\").contains(\"pgc\")\n        val dmViewReply = if (generateSubtitle || (addCloseSubtitle && fromPGC)) {\n            parseDmViewReply(originalReply)\n        } else null\n        if (generateSubtitle) {\n            val subtitles = mutableListOf<SubtitleItem>()\n            dmViewReply?.subtitle?.subtitlesList?.let { subtitles += it }\n            subtitles += extraSubtitles\n            if (subtitles.map { it.lan }.let { \"zh-Hant\" in it && \"zh-CN\" !in it }) {\n                val origSub = subtitles.first { it.lan == \"zh-Hant\" }\n                val targetSubUrl = Uri.parse(origSub.subtitleUrl).buildUpon()\n                    .appendQueryParameter(\"zh_converter\", \"t2cn\")\n                    .build().toString()\n\n                subtitleItem {\n                    lan = \"zh-CN\"\n                    lanDoc = \"简中（生成）\"\n                    lanDocBrief = \"简中\"\n                    subtitleUrl = targetSubUrl\n                    id = origSub.id + 1\n                    idStr = id.toString()\n                }.let { extraSubtitles += it }\n            }\n        }\n\n        if (addCloseSubtitle && fromPGC\n            && (!dmViewReply?.subtitle?.subtitlesList.isNullOrEmpty() || extraSubtitles.isNotEmpty())\n        ) {\n            subtitleItem {\n                lan = \"nodisplay\"\n                lanDoc = closeText\n            }.let { extraSubtitles += it }\n        }\n\n        if (extraSubtitles.isNotEmpty()) {\n            val newRes = (dmViewReply ?: parseDmViewReply(originalReply)\n            ?: dmViewReply {}).copy {\n                subtitle = subtitle.copy {\n                    subtitles += extraSubtitles\n                }\n                d = tryThailand\n                inputPlaceHolder = \"泰区不支持\"\n            }\n            return originalReply?.javaClass?.callStaticMethod(\"parseFrom\", newRes.toByteArray())\n        }\n\n        return null\n    }\n\n    private fun JSONArray.toSubtitles(): List<SubtitleItem> {\n        val subList = mutableListOf<SubtitleItem>()\n        for (subtitle in this) {\n            subtitleItem {\n                id = subtitle.optLong(\"id\")\n                idStr = subtitle.optLong(\"id\").toString()\n                subtitleUrl = subtitle.optString(\"url\")\n                lan = subtitle.optString(\"key\")\n                lanDoc = subtitle.optString(\"title\")\n            }.let { subList.add(it) }\n        }\n        return subList\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/TeenagersModeHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.app.Activity\nimport android.os.Bundle\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.hookAfterMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\n/**\n * Created by iAcn on 2019/12/15\n * Email i@iacn.me\n */\nclass TeenagersModeHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"teenagers_mode_dialog\", false)) return\n        Log.d(\"startHook: TeenagersMode\")\n        instance.teenagersModeDialogActivityClass?.hookAfterMethod(\n            \"onCreate\", Bundle::class.java\n        ) { param ->\n            val activity = param.thisObject as Activity\n            activity.finish()\n            Log.d(\"Teenagers mode dialog has been closed\")\n            Log.toast(\"已关闭青少年模式对话框\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/TryWatchVipQualityHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.replaceMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\nclass TryWatchVipQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"disable_try_watch_vip_quality\", false)) return\n\n        instance.vipQualityTrialService?.replaceMethod(instance.canTrialMethod()) { false }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/UposReplaceHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.UposReplaceHelper.enableLivePcdnBlock\nimport me.iacn.biliroaming.utils.UposReplaceHelper.enablePcdnBlock\nimport me.iacn.biliroaming.utils.UposReplaceHelper.enableUposReplace\nimport me.iacn.biliroaming.utils.UposReplaceHelper.forceUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.gotchaRegex\nimport me.iacn.biliroaming.utils.UposReplaceHelper.initVideoUposList\nimport me.iacn.biliroaming.utils.UposReplaceHelper.isNeedReplaceVideoUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.isOverseaUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.isPCdnUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.liveUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.replaceUpos\nimport me.iacn.biliroaming.utils.UposReplaceHelper.videoUposBackups\nimport me.iacn.biliroaming.utils.from\nimport me.iacn.biliroaming.utils.getObjectFieldOrNull\nimport me.iacn.biliroaming.utils.getObjectFieldOrNullAs\nimport me.iacn.biliroaming.utils.hookBeforeConstructor\nimport me.iacn.biliroaming.utils.hookBeforeMethod\nimport me.iacn.biliroaming.utils.setObjectField\n\n\nclass UposReplaceHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!enableUposReplace || !(forceUpos || enablePcdnBlock || enableLivePcdnBlock)) return\n        Log.d(\"startHook: UposReplaceHook\")\n        \"tv.danmaku.ijk.media.player.IjkMediaAsset\\$MediaAssertSegment\\$Builder\".from(mClassLoader)\n            ?.run {\n                hookBeforeConstructor(String::class.java, Int::class.javaPrimitiveType) { param ->\n                    val baseUrl = param.args[0] as String\n                    if (baseUrl.contains(\"live-bvc\")) {\n                        if (enableLivePcdnBlock && !baseUrl.contains(gotchaRegex)) {\n                            param.args[0] = baseUrl.replaceUpos(liveUpos)\n                        }\n                    } else if (baseUrl.isNeedReplaceVideoUpos()) {\n                        param.args[0] = baseUrl.replaceUpos()\n                    }\n                }\n\n                if (!(enablePcdnBlock || forceUpos)) return@run\n                hookBeforeMethod(\"setBackupUrls\", MutableCollection::class.java) { param ->\n                    val mediaAssertSegment = param.thisObject.getObjectFieldOrNull(\"target\")\n                    val baseUrl =\n                        mediaAssertSegment?.getObjectFieldOrNullAs<String>(\"url\").orEmpty()\n                    if (baseUrl.isEmpty()) return@hookBeforeMethod\n                    val backupUrls = if (param.args[0] == null) {\n                        if (baseUrl.contains(\"live-bvc\")) return@hookBeforeMethod else {\n                            emptyList<String>()\n                        }\n                    } else {\n                        @Suppress(\"UNCHECKED_CAST\")\n                        (param.args[0] as List<String>).filter { !it.isPCdnUpos() }\n                            .takeIf { backupUrls ->\n                                backupUrls.isEmpty() || !backupUrls.any { it.contains(\"live-bvc\") }\n                            } ?: return@hookBeforeMethod\n                    }\n                    reconstructBackupUposList(\n                        baseUrl, backupUrls, mediaAssertSegment\n                    ).takeIf { it.isNotEmpty() }?.let {\n                        param.args[0] = it\n                    }\n                }\n            }\n    }\n\n    override fun lateInitHook() {\n        initVideoUposList(mClassLoader)\n    }\n\n    private fun reconstructBackupUposList(\n        baseUrl: String, backupUrls: List<String>, mediaAssertSegment: Any?\n    ): List<String> {\n        val rawUrl = backupUrls.firstOrNull() ?: baseUrl\n        return if (baseUrl.isPCdnUpos()) {\n            if (backupUrls.isNotEmpty()) {\n                mediaAssertSegment?.setObjectField(\"url\", rawUrl.replaceUpos())\n                listOf(rawUrl.replaceUpos(videoUposBackups[0], rawUrl.isOverseaUpos()), baseUrl)\n            } else emptyList()\n        } else {\n            if (enablePcdnBlock || forceUpos || backupUrls.isEmpty() || rawUrl.isOverseaUpos()) {\n                listOf(\n                    rawUrl.replaceUpos(videoUposBackups[0]), rawUrl.replaceUpos(videoUposBackups[1])\n                )\n            } else emptyList()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/VideoQualityHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.*\n\nclass VideoQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"main_func\", false)) return\n\n        val halfScreenQuality = sPrefs.getString(\"half_screen_quality\", \"0\")?.toInt() ?: 0\n        val fullScreenQuality = sPrefs.getString(\"full_screen_quality\", \"0\")?.toInt() ?: 0\n        if (halfScreenQuality != 0) {\n            instance.playerPreloadHolderClass?.replaceAllMethods(instance.getPreload()) { null }\n            instance.playerQualityServices().forEach { (clazz, getDefaultQnThumb) ->\n                clazz?.replaceAllMethods(getDefaultQnThumb) { halfScreenQuality }\n            }\n        }\n        if (fullScreenQuality != 0) {\n            instance.playerSettingHelperClass?.replaceMethod(instance.getDefaultQn()) { fullScreenQuality }\n        }\n\n        if (halfScreenQuality != 0 || fullScreenQuality != 0) {\n            instance.autoSupremumQualityClass?.hookBeforeConstructor(\n                *Array(6) { Int::class.javaPrimitiveType }\n            ) { param ->\n                if (halfScreenQuality != 0) {\n                    param.args[0] = halfScreenQuality       // loginHalf\n\n                    param.args[3] = halfScreenQuality       // unloginHalf\n                    param.args[4] = halfScreenQuality       // unloginFull\n                    param.args[5] = halfScreenQuality       // unloginMobileFull\n                }\n                if (fullScreenQuality != 0) {\n                    param.args[1] = fullScreenQuality       // loginFull\n                    param.args[2] = fullScreenQuality       // loginMobileFull\n                }\n            }\n            instance.qualityStrategyProviderClass?.hookBeforeMethod(\n                instance.selectQuality(),\n                instance.autoSupremumQualityClass,\n                Boolean::class.javaPrimitiveType,           // isFullscreen\n                Boolean::class.javaPrimitiveType            // isVideoPortrait\n            ) { param ->\n                // videoQuality = when {\n                //     isVideoPortrait && isFullscreen -> loginFull\n                //     isVideoPortrait && !isFullscreen -> unloginFull\n                //     isFullscreen -> loginHalf\n                //     else -> unloginHalf\n                // }\n                param.args[2] = true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/VipSectionHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.os.Bundle\nimport android.view.View\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.hookAfterMethod\nimport me.iacn.biliroaming.utils.sPrefs\n\n\nclass VipSectionHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    override fun startHook() {\n        if (!sPrefs.getBoolean(\"hidden\", false)\n            || !sPrefs.getBoolean(\"remove_vip_section\", false)\n        ) return\n\n        instance.homeUserCenterClass!!.hookAfterMethod(\n            \"onViewCreated\",\n            View::class.java,\n            Bundle::class.java\n        ) {\n            val obj = it.thisObject\n            val vipModuleManager = instance.homeUserCenterClass!!.declaredFields.single {\n                // $mineVipModuleManager\n                it.type.toString().contains(\"MineVipModuleManager\")\n            }.run {\n                isAccessible = true\n                get(obj)\n            }\n\n            vipModuleManager::class.java.declaredMethods.single {\n                // $method(isTeenager: Boolean)\n                it.parameterCount == 1 &&\n                        it.parameterTypes[0] == Boolean::class.java\n            }.run {\n                isAccessible = true\n                invoke(vipModuleManager, true)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/hook/WebViewHook.kt",
    "content": "package me.iacn.biliroaming.hook\n\nimport android.graphics.Bitmap\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.launch\nimport me.iacn.biliroaming.BuildConfig\nimport me.iacn.biliroaming.XposedInit.Companion.moduleRes\nimport me.iacn.biliroaming.utils.*\n\n\nclass WebViewHook(classLoader: ClassLoader) : BaseHook(classLoader) {\n    private val hookedClient = HashSet<Class<*>>()\n\n    private val jsHooker = object : Any() {\n        @Suppress(\"UNUSED\")\n        @JavascriptInterface\n        fun hook(url: String, text: String): String {\n            return this@WebViewHook.hook(url, text)\n        }\n\n        @Suppress(\"UNUSED\")\n        @JavascriptInterface\n        fun saveImage(url: String) {\n            MainScope().launch(Dispatchers.IO) {\n                CommentImageHook.saveImage(url)\n            }\n        }\n    }\n\n    private val js by lazy {\n        runCatchingOrNull {\n            moduleRes.assets.open(\"xhook.js\")\n                .use { it.bufferedReader().readText() }\n        } ?: \"\"\n    }\n\n    override fun startHook() {\n        Log.d(\"startHook: WebView\")\n        WebView::class.java.hookBeforeMethod(\n            \"setWebViewClient\", WebViewClient::class.java\n        ) { param ->\n            val clazz = param.args[0].javaClass\n            (param.thisObject as WebView).addJavascriptInterface(jsHooker, \"hooker\")\n            if (hookedClient.contains(clazz)) return@hookBeforeMethod\n            try {\n                clazz.getDeclaredMethod(\n                    \"onPageStarted\",\n                    WebView::class.java, String::class.java, Bitmap::class.java\n                ).hookBeforeMethod { p ->\n                    val webView = p.args[0] as WebView\n                    webView.evaluateJavascript(\"\"\"(function(){$js})()\"\"\".trimMargin(), null)\n                }\n                if (sPrefs.getBoolean(\"save_comment_image\", false)) {\n                    clazz.getDeclaredMethod(\n                        \"onPageFinished\",\n                        WebView::class.java, String::class.java\n                    ).hookBeforeMethod { p ->\n                        val webView = p.args[0] as WebView\n                        val url = p.args[1] as String\n                        if (url.startsWith(\"https://www.bilibili.com/h5/note-app/view\")) {\n                            webView.evaluateJavascript(\n                                \"\"\"(function(){for(var i=0;i<document.images.length;++i){if(document.images[i].className==='img-preview'){document.images[i].addEventListener(\"contextmenu\",(e)=>{hooker.saveImage(e.target.currentSrc);})}}})()\"\"\",\n                                null\n                            )\n                        }\n                    }\n                }\n                hookedClient.add(clazz)\n                Log.d(\"hook webview $clazz\")\n            } catch (_: NoSuchMethodException) {\n            }\n        }\n    }\n\n    @Suppress(\"UNUSED_PARAMETER\")\n    fun hook(url: String, text: String): String {\n        return text\n    }\n\n    override fun lateInitHook() {\n        if (BuildConfig.DEBUG) {\n            WebView.setWebContentsDebuggingEnabled(true)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/network/BiliRoamingApi.kt",
    "content": "package me.iacn.biliroaming.network\n\nimport android.annotation.SuppressLint\nimport android.app.AndroidAppHelper\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Handler\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.BuildConfig\nimport me.iacn.biliroaming.XposedInit\nimport me.iacn.biliroaming.hook.BangumiSeasonHook.Companion.lastSeasonInfo\nimport me.iacn.biliroaming.utils.*\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.io.IOException\nimport java.io.InputStream\nimport java.net.HttpURLConnection\nimport java.net.URL\nimport java.util.concurrent.CountDownLatch\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicReference\nimport java.util.zip.GZIPInputStream\nimport java.util.zip.InflaterInputStream\n\n\n/**\n * Created by iAcn on 2019/3/27\n * Email i@iacn.me\n */\nobject BiliRoamingApi {\n    private const val BILI_SEASON_URL = \"api.bilibili.com/pgc/view/v2/app/season\"\n    private const val BILI_SEARCH_URL = \"/x/v2/search/type\"\n    private const val BILIPLUS_VIEW_URL = \"www.biliplus.com/api/view\"\n    private const val BILI_MEDIA_URL = \"www.bilibili.com/bangumi/media/md\"\n    private const val BILI_SECTION_URL = \"api.bilibili.com/pgc/web/season/section\"\n    private const val BILI_CARD_URL = \"https://account.bilibili.com/api/member/getCardByMid\"\n    private const val BILI_PAGELIST = \"api.bilibili.com/x/player/pagelist\"\n    private const val BILI_MODULE_TEMPLATE =\n        \"{\\\"data\\\": {},\\\"id\\\": 0,\\\"module_style\\\": {\\\"hidden\\\": 0,\\\"line\\\": 1},\\\"more\\\": \\\"查看更多\\\",\\\"style\\\": \\\"positive\\\",\\\"title\\\": \\\"选集\\\"}\"\n    private const val BILI_RIGHT_TEMPLATE =\n        \"{\\\"allow_demand\\\":0,\\\"allow_dm\\\":1,\\\"allow_download\\\":0,\\\"area_limit\\\":0}\"\n    private const val BILI_VIP_BADGE_TEMPLATE =\n        \"{\\\"bg_color\\\":\\\"#FB7299\\\",\\\"bg_color_night\\\":\\\"#BB5B76\\\",\\\"text\\\":\\\"%s\\\"}\"\n\n    private const val PATH_PLAYURL = \"/pgc/player/api/playurl\"\n    private const val THAILAND_PATH_PLAYURL = \"/intl/gateway/v2/ogv/playurl\"\n    private const val THAILAND_PATH_SUBTITLES = \"/intl/gateway/v2/app/subtitle\"\n    private const val THAILAND_PATH_SEARCH = \"/intl/gateway/v2/app/search/type\"\n    private const val THAILAND_PATH_SEASON = \"/intl/gateway/v2/ogv/view/app/season\"\n\n    const val overseaTestParams =\n        \"cid=120453316&ep_id=285145&otype=json&fnval=16&module=pgc&platform=android&test=true\"\n    const val mainlandTestParams =\n        \"cid=13073143&ep_id=100615&otype=json&fnval=16&module=pgc&platform=android&test=true\"\n\n    private val seasonCache: AtomicReference<Triple<Int, AtomicReference<String?>, CountDownLatch>?> =\n        AtomicReference(null)\n\n    @JvmStatic\n    fun getSeason(info: Map<String, String?>, season: JSONObject?): String? {\n        val seasonId = info.getOrDefault(\"season_id\", null)?.toInt() ?: return null\n        val cache = seasonCache.get()\n        val cacheTuple = if (seasonId != 0) {\n            if (cache?.first == seasonId) {\n                cache.third.await()\n                return cache.second.get()\n            } else {\n                Triple(seasonId, AtomicReference<String?>(null), CountDownLatch(1)).also {\n                    seasonCache.compareAndSet(cache, it)\n                }\n            }\n        } else null\n\n        var seasonJson = season\n\n        seasonJson = seasonJson ?: run {\n            val query = mapOf(\n                    \"season_id\" to seasonId.toString(),\n            )\n            val content = getContent(Uri.Builder().scheme(\"https\").encodedAuthority(BILI_SEASON_URL).encodedQuery(signQuery(query)).toString())\n            content?.toJSONObject()?.optJSONObject(\"data\")\n        } ?: run {\n            val content = getContent(Uri.Builder().scheme(\"https\").encodedAuthority(\"${BILI_MEDIA_URL}${seasonId}\").toString()) ?: return@run null\n            val json = Regex(\"\"\"[\\w\\W]*window\\.__INITIAL_STATE__=(.*);\\(function\\(\\)[\\w\\W]*\"\"\").matchEntire(content)?.groupValues?.get(1)?.toJSONObject() ?: return@run null\n            json.optJSONObject(\"mediaInfo\")?.also {\n                it.put(\"modules\", JSONArray().put(JSONObject().put(\"data\", JSONObject().put(\"seasons\", it.optJSONObject(\"seasons\")))))\n            }\n        }\n\n        seasonJson = seasonJson?.let {\n            fixSection(it)\n            fixEpisodes(it)\n            fixPrevueSection(it)\n            reconstructModules(it)\n            fixRight(it)\n            JSONObject().put(\"code\", 0).put(\"result\", it)\n        } ?: run {\n            val thUrl = sPrefs.getString(\"th_server\", null)\n            val mobiApp = sPrefs.getString(\"th_server_platform\", platform)!!\n            if (thUrl != null) {\n                val builder = Uri.Builder()\n                builder.scheme(\"https\").encodedAuthority(thUrl)\n                        .encodedPath(THAILAND_PATH_SEASON)\n                        .appendQueryParameter(\"s_locale\", \"zh_SG\")\n                        .appendQueryParameter(\"access_key\", instance.getCustomizeAccessKey(\"th_server\"))\n                        .appendQueryParameter(\"mobi_app\", \"bstar_a\")\n                        .appendQueryParameter(\"build\", \"1080003\")\n                        .appendQueryParameter(\"season_id\", seasonId.toString())\n                        .appendQueryParameter(\"ep_id\", info.getOrDefault(\"ep_id\", \"0\"))\n                getContent(builder.toString(), mobiApp)?.toJSONObject()?.also {\n                    it.optJSONObject(\"result\")?.let { result ->\n                        fixThailandSeason(result)\n                        return@run it\n                    }\n                    checkErrorToast(it, true)\n                }\n            } else {\n                checkErrorToast(\"\"\"{\"code\" = -404, \"message\" = \"未设置泰区解析服务器\"}\"\"\".toJSONObject())\n            }\n            null\n        }\n        return seasonJson?.toString().also {\n            if (seasonJson?.optInt(\"code\", -1) == 0)\n                cacheTuple?.second?.set(it)\n            cacheTuple?.third?.countDown()\n        }\n    }\n\n    @JvmStatic\n    private fun fixSection(result: JSONObject) {\n        val seasonId = result.optString(\"season_id\")\n        val uri = Uri.Builder()\n            .scheme(\"https\")\n            .encodedAuthority(BILI_SECTION_URL)\n            .appendQueryParameter(\"season_id\", seasonId)\n            .toString()\n        val sectionJson = getContent(uri).toJSONObject().optJSONObject(\"result\") ?: return\n        val sections = sectionJson.optJSONArray(\"section\") ?: return\n\n        for ((i, section) in sections.iterator().withIndex()) {\n            section.put(\"episode_id\", i)\n            val newEpisodes = JSONArray()\n            for (episode in section.optJSONArray(\"episodes\").orEmpty()) {\n                newEpisodes.put(episode)\n            }\n            section.put(\"episodes\", newEpisodes)\n        }\n        result.put(\"section\", sections)\n        result.optJSONObject(\"newest_ep\")?.run {\n            put(\"title\", optString(\"index\"))\n            result.put(\"new_ep\", this)\n        }\n\n        val newEpisodes = JSONArray()\n        for (episode in sectionJson.optJSONObject(\"main_section\")?.optJSONArray(\"episodes\")\n            .orEmpty()) {\n            newEpisodes.put(episode)\n        }\n        result.put(\"episodes\", newEpisodes)\n    }\n\n    @JvmStatic\n    fun getPagelist(aid: String) = getContent(\n        Uri.Builder().scheme(\"https\")\n            .encodedAuthority(BILI_PAGELIST)\n            .appendQueryParameter(\"aid\", aid).toString()\n    )?.toJSONObject()?.let {\n        if (it.optInt(\"code\", -1) == 0) it else null\n    }\n\n    @JvmStatic\n    fun getThailandSubtitles(epId: String?): String? {\n        Log.d(\"Getting subtitle $epId form thailand\")\n        epId ?: return null\n        val thUrl = sPrefs.getString(\"th_server\", null) ?: return null\n        val uri = Uri.Builder()\n            .scheme(\"https\")\n            .encodedAuthority(thUrl)\n            .encodedPath(THAILAND_PATH_SUBTITLES)\n            .appendQueryParameter(\"ep_id\", epId)\n            .toString()\n        return getContent(uri)\n    }\n\n    @JvmStatic\n    fun getAreaSearchBangumi(query: Map<String, String>, area: String, type: String): String? {\n        if (area == \"th\") {\n            return getThailandSearchBangumi(query, type)\n        }\n        val hostUrl = sPrefs.getString(area + \"_server\", null) ?: return null\n        val uri = Uri.Builder()\n            .scheme(\"https\")\n            .encodedAuthority(hostUrl)\n            .encodedPath(BILI_SEARCH_URL)\n            .encodedQuery(\n                signQuery(\n                    query, mapOf(\n                        \"type\" to type,\n                        \"build\" to \"6400000\",\n                        \"area\" to area,\n                    )\n                )\n            )\n            .toString()\n        return getContent(uri)\n    }\n\n    @JvmStatic\n    fun getThailandSearchBangumi(query: Map<String, String>, type: String): String? {\n        val thUrl = sPrefs.getString(\"th_server\", null) ?: return null\n        val uri = Uri.Builder()\n            .scheme(\"https\")\n            .encodedAuthority(thUrl)\n            .encodedPath(THAILAND_PATH_SEARCH)\n            .encodedQuery(\n                signQuery(\n                    query, mapOf(\n                        \"type\" to type,\n                        \"appkey\" to \"7d089525d3611b1c\",\n                        \"build\" to \"1001310\",\n                        \"mobi_app\" to \"bstar_a\",\n                        \"platform\" to \"android\",\n                        \"s_locale\" to \"zh_SG\",\n                        \"c_locale\" to \"zh_SG\",\n                        \"sim_code\" to \"52004\",\n                        \"lang\" to \"hans\",\n                    )\n                )\n            )\n            .toString()\n        return getContent(uri)?.replace(\n            \"bstar://bangumi/season/\",\n            \"https://bangumi.bilibili.com/anime/\"\n        )\n    }\n\n    @JvmStatic\n    private fun fixPrevueSection(result: JSONObject) {\n        result.put(\"prevueSection\", result.optJSONObject(\"section\"))\n    }\n\n    @JvmStatic\n    private fun fixEpisodes(result: JSONObject, sid: Int = 0) {\n        val episodes = result.optJSONArray(\"episodes\")\n        for ((eid, episode) in episodes.orEmpty().iterator().withIndex()) {\n            fixRight(episode)\n            if (episode.optInt(\"badge_type\", -1) == 0)\n                episode.remove(\"badge_info\")\n            if (episode.optString(\"badge\") != \"受限\")\n                episode.put(\n                    \"badge_info\",\n                    JSONObject(BILI_VIP_BADGE_TEMPLATE.format(episode.optString(\"badge\")))\n                )\n            episode.put(\"ep_index\", eid + 1)\n            episode.put(\"section_index\", sid + 1)\n        }\n        for ((off, section) in result.optJSONArray(\"section\").orEmpty().iterator().withIndex()) {\n            fixEpisodes(section, sid + off + 1)\n        }\n    }\n\n    @JvmStatic\n    private fun reconstructModules(result: JSONObject) {\n        var id = 0\n        val module = BILI_MODULE_TEMPLATE.toJSONObject()\n        val episodes = result.optJSONArray(\"episodes\")\n        module.optJSONObject(\"data\")?.put(\"episodes\", episodes)\n        module.put(\"id\", ++id)\n        val modules = arrayListOf(module)\n\n        if (result.has(\"section\")) {\n            val sections = result.optJSONArray(\"section\")\n            for (section in sections.orEmpty()) {\n                val sectionModule = BILI_MODULE_TEMPLATE.toJSONObject()\n                    .put(\"data\", section)\n                    .put(\"style\", \"section\")\n                    .put(\"title\", section.optString(\"title\"))\n                    .put(\"id\", ++id)\n                modules.add(sectionModule)\n            }\n        }\n        if (result.has(\"seasons\")) {\n            val seasons = result.optJSONArray(\"seasons\")\n            for (season in seasons.orEmpty()) {\n                season.put(\"title\", season.optString(\"season_title\"))\n            }\n            val seasonModule = BILI_MODULE_TEMPLATE.toJSONObject()\n            seasonModule.put(\"data\", JSONObject().put(\"seasons\", seasons))\n                .put(\"style\", \"season\")\n                .put(\"title\", \"\")\n                .put(\"id\", ++id)\n                .put(\"module_style\", JSONObject(\"{\\\"line\\\": 1}\"))\n            modules.add(seasonModule)\n\n        }\n        // work around\n        result.put(\"modules\", JSONArray(modules))\n    }\n\n    @JvmStatic\n    private fun fixRight(result: JSONObject) {\n        result.optJSONObject(\"rights\")?.run {\n            put(\"area_limit\", 0)\n            put(\"allow_dm\", 1)\n        } ?: run { result.put(\"rights\", BILI_RIGHT_TEMPLATE.toJSONObject()) }\n    }\n    class CustomServerException(private val errors: Map<String, String>) : Throwable() {\n        override val message: String\n            get() = errors.asSequence().joinToString(\"\\n\") { \"${it.key}: ${it.value}\" }.trim()\n    }\n\n    @JvmStatic\n    fun getPlayUrl(queryString: String?, priorityArea: Array<String>? = null): String? {\n        queryString ?: return null\n        val twUrl = sPrefs.getString(\"tw_server\", null)\n        val hkUrl = sPrefs.getString(\"hk_server\", null)\n        val cnUrl = sPrefs.getString(\"cn_server\", null)\n        val thUrl = sPrefs.getString(\"th_server\", null)\n\n        val hostList = LinkedHashMap<String, String>(4, 1f, true)\n\n        if (hostList.isEmpty())\n        // reversely\n            linkedMapOf(\"tw\" to twUrl, \"hk\" to hkUrl, \"th\" to thUrl, \"cn\" to cnUrl).filterKeys {\n                if (!sPrefs.getString(\"${it}_server_accessKey\", null).isNullOrEmpty())\n                    return@filterKeys true\n                it != (runCatchingOrNull { XposedInit.country.get(5L, TimeUnit.SECONDS) } ?: true)\n            }.filterValues {\n                it != null\n            }.mapValuesTo(hostList) {\n                it.value!!\n            }\n\n        val epIdStartIdx = queryString.indexOf(\"ep_id=\")\n        val epIdEndIdx = queryString.indexOf(\"&\", epIdStartIdx)\n        val epId = queryString.substring(epIdStartIdx + 6, epIdEndIdx)\n\n        if (!lastSeasonInfo.containsKey(\"ep_ids\") || lastSeasonInfo[\"ep_ids\"]?.contains(epId) != true)\n            lastSeasonInfo.clear()\n\n        lastSeasonInfo[\"title\"]?.run {\n            if (contains(Regex(\"僅.*台\")) && twUrl != null) hostList[\"tw\"]\n            if (contains(Regex(\"僅.*港\")) && hkUrl != null) hostList[\"hk\"]\n            if (contains(Regex(\"[仅|僅].*[东南亚|其他]\")) && thUrl != null) hostList[\"th\"]\n        }\n\n        priorityArea?.forEach { area ->\n            if (hostList.containsKey(area)) hostList[area]\n        }\n\n        if (hostList.isEmpty()) return null\n\n        val seasonId = lastSeasonInfo[\"season_id\"] ?: if (epId.isEmpty()) null else \"ep$epId\"\n\n        if (seasonId != null && sCaches.contains(seasonId)) {\n            val cachedArea = sCaches.getString(seasonId, null)\n            if (hostList.containsKey(cachedArea)) {\n                Log.d(\"use cached area $cachedArea for $seasonId\")\n                hostList[cachedArea]\n            }\n        }\n\n        val errors: MutableMap<String, String> = mutableMapOf()\n\n        for ((area, host) in hostList.toList().asReversed()) {\n            val accessKey = instance.getCustomizeAccessKey(\"${area}_server\") ?: \"\"\n            val mobiApp = sPrefs.getString(\"${area}_server_platform\", platform)!!\n            val extraMap = if (area == \"th\") mapOf(\n                \"area\" to area,\n                \"appkey\" to \"7d089525d3611b1c\",\n                \"build\" to \"1001310\",\n                \"mobi_app\" to \"bstar_a\",\n                \"platform\" to \"android\",\n                \"access_key\" to accessKey,\n            )\n            else mapOf(\n                \"area\" to area,\n                \"access_key\" to accessKey,\n            )\n            val path = if (area == \"th\") THAILAND_PATH_PLAYURL else PATH_PLAYURL\n            val uri = Uri.Builder()\n                .scheme(\"https\")\n                .encodedAuthority(host)\n                .encodedPath(path)\n                .encodedQuery(signQuery(queryString, extraMap))\n                .toString()\n            getContent(uri, mobiApp)?.let {\n                Log.d(\"use server $area $host for playurl\")\n                if (it.contains(\"\\\"code\\\":0\")) {\n                    lastSeasonInfo[\"area\"] = area\n                    lastSeasonInfo[\"epid\"] = epId\n                    if (seasonId != null && !sCaches.contains(seasonId) || sCaches.getString(\n                            seasonId,\n                            null\n                        ) != area\n                    ) {\n                        sCaches.edit().run {\n                            putString(seasonId, area)\n                            lastSeasonInfo[\"ep_ids\"]?.split(\";\")\n                                ?.forEach { epId -> putString(\"ep$epId\", area) }\n                            apply()\n                        }\n                    }\n                    return if (area == \"th\") fixThailandPlayurl(it) else it\n                }\n                errors.put(area, JSONObject(it).optString(\"message\"))\n            } ?: errors.putIfAbsent(area, \"服务器不可用\")\n        }\n        throw CustomServerException(errors)\n    }\n\n    @JvmStatic\n    fun fixThailandPlayurl(result: String): String {\n        val input = JSONObject(result)\n        val videoInfo = result.toJSONObject().optJSONObject(\"data\")?.optJSONObject(\"video_info\")\n        val streamList = videoInfo?.optJSONArray(\"stream_list\")\n        val dashAudio = videoInfo?.optJSONArray(\"dash_audio\")\n\n        val output = JSONObject().apply {\n            put(\"format\", \"flv720\")\n            put(\"type\", \"DASH\")\n            put(\"result\", \"suee\")\n            put(\"video_codecid\", 7)\n            put(\"no_rexcode\", 0)\n\n            put(\"code\", input.optInt(\"code\"))\n            put(\"message\", input.optInt(\"message\"))\n            put(\"timelength\", videoInfo?.optInt(\"timelength\"))\n            put(\"quality\", videoInfo?.optInt(\"quality\"))\n            put(\"accept_format\", \"hdflv2_4k,hdflv2_hdr,hdflv2_dolby,hdflv2,flv,flv720,flv480,mp4\")\n        }\n\n        val acceptQuality = JSONArray()\n        val acceptDescription = JSONArray()\n\n        val dash = JSONObject().apply {\n            put(\"duration\", 0)\n            put(\"minBufferTime\", 0.0)\n            put(\"min_buffer_time\", 0.0)\n        }\n        val fixedAudio = JSONArray().apply {\n            for (audio in dashAudio.orEmpty()) {\n                put(audio)\n            }\n        }\n        dash.put(\"audio\", fixedAudio)\n\n        val supportFormats = JSONArray()\n        val dashVideo = JSONArray()\n        for (stream in streamList.orEmpty()) {\n            if (stream.optJSONObject(\"dash_video\")?.optString(\"base_url\").isNullOrBlank()) {\n                continue\n            }\n            stream.optJSONObject(\"stream_info\")?.let {\n                supportFormats.put(it)\n            }\n            stream.optJSONObject(\"stream_info\")?.let {\n                acceptQuality.put(it.optInt(\"quality\"))\n            }\n            stream.optJSONObject(\"stream_info\")?.let {\n                acceptDescription.put(it.optString(\"new_description\"))\n            }\n            stream.optJSONObject(\"dash_video\")?.let {\n                it.put(\"id\", stream.optJSONObject(\"stream_info\")?.optInt(\"quality\"))\n                dashVideo.put(it)\n            }\n        }\n        dash.put(\"video\", dashVideo)\n\n        output.put(\"accept_quality\", acceptQuality)\n        output.put(\"accept_description\", acceptDescription)\n        output.put(\"support_formats\", supportFormats)\n        output.put(\"dash\", dash)\n\n        return output.toString()\n    }\n\n    @JvmStatic\n    fun fixThailandSeason(result: JSONObject) {\n        val episodes = JSONArray()\n\n        // 强制已追番\n        result.optJSONObject(\"user_status\")?.put(\"follow\", 1)\n\n        for ((mid, module) in result.optJSONArray(\"modules\").orEmpty().iterator().withIndex()) {\n            val data = module.optJSONObject(\"data\") ?: continue\n            val sid = module.optInt(\"id\", mid + 1)\n            for ((eid, ep) in data.optJSONArray(\"episodes\").orEmpty()\n                .iterator().withIndex()) {\n                if (ep.optInt(\"status\") == 13) {\n                    ep.put(\"badge\", \"泰区会员\")\n                    ep.put(\"badge_info\", JSONObject().apply {\n                        put(\"bg_color\", \"#FB7299\")\n                        put(\"bg_color_night\", \"#BB5B76\")\n                        put(\"text\", \"泰区会员\")\n                    })\n                }\n                ep.put(\"status\", 2)\n                ep.put(\"episode_status\", 2)\n                ep.put(\"ep_id\", ep.optInt(\"id\"))\n                ep.put(\"index\", ep.optString(\"title\"))\n                ep.put(\"link\", \"https://www.bilibili.com/bangumi/play/ep${ep.optInt(\"id\")}\")\n                ep.put(\"indexTitle\", ep.optString(\"long_title\"))\n                ep.put(\"ep_index\", eid + 1)\n                ep.put(\"section_index\", sid + 1)\n                fixRight(ep)\n                if (ep.optInt(\"cid\", 0) == 0) {\n                    ep.put(\"cid\", ep.optInt(\"id\"))\n                    if (!sPrefs.getBoolean(\"force_th_comment\", false))\n                        ep.optJSONObject(\"rights\")?.put(\"allow_dm\", 0)\n                }\n                if (ep.optInt(\"aid\", 0) == 0) {\n                    ep.put(\"aid\", result.optInt(\"season_id\"))\n                    if (!sPrefs.getBoolean(\"force_th_comment\", false))\n                        ep.optJSONObject(\"rights\")?.put(\"area_limit\", 1)\n                }\n                ep.put(\"duration\", 114514)\n                episodes.put(ep)\n            }\n            data.put(\"id\", sid)\n        }\n\n        result.put(\"episodes\", episodes)\n        val style = JSONArray()\n        for (i in result.optJSONArray(\"styles\").orEmpty()) {\n            style.put(i.optString(\"name\"))\n        }\n        result.put(\"style\", style)\n        result.optJSONObject(\"rights\")?.put(\"watch_platform\", 1)\n            ?.put(\"allow_comment\", 0)\n        result.apply {\n            put(\"actors\", result.optJSONObject(\"actor\")?.optString(\"info\"))\n            put(\"is_paster_ads\", 0)\n            put(\"jp_title\", result.optString(\"origin_name\"))\n            put(\"newest_ep\", result.optJSONObject(\"new_ep\"))\n            put(\"season_status\", result.optInt(\"status\"))\n            put(\"season_title\", result.optString(\"title\"))\n            put(\"total_ep\", episodes.length())\n        }\n    }\n\n    @JvmStatic\n    fun getView(queryString: String?): String? {\n        val builder = Uri.Builder()\n        builder.scheme(\"https\").encodedAuthority(BILIPLUS_VIEW_URL)\n        builder.encodedQuery(queryString)\n        builder.appendQueryParameter(\"module\", \"bangumi\")\n        builder.appendQueryParameter(\"otype\", \"json\")\n        builder.appendQueryParameter(\"platform\", \"android\")\n        return getContent(builder.toString())\n    }\n\n    @JvmStatic\n    fun getSpace(mid: Long): String? {\n        val content = getContent(\"$BILI_CARD_URL?mid=$mid\").toJSONObject()\n        if (content.optInt(\"code\") != 0) return null\n        val card = content.optJSONObject(\"card\") ?: return null\n        val levelInfo = card.optJSONObject(\"level_info\") ?: return null\n        val officialVerify = card.optJSONObject(\"official_verify\") ?: return null\n        return \"\"\"{\"relation\":-999,\"guest_relation\":-999,\"default_tab\":\"video\",\"is_params\":true,\"setting\":{\"fav_video\":0,\"coins_video\":0,\"likes_video\":0,\"bangumi\":0,\"played_game\":0,\"groups\":0,\"comic\":0,\"bbq\":0,\"dress_up\":0,\"disable_following\":0,\"live_playback\":1,\"close_space_medal\":0,\"only_show_wearing\":0},\"tab\":{\"archive\":true,\"article\":true,\"clip\":true,\"album\":true,\"favorite\":false,\"bangumi\":false,\"coin\":false,\"like\":false,\"community\":false,\"dynamic\":true,\"audios\":true,\"shop\":false,\"mall\":false,\"ugc_season\":false,\"comic\":false,\"cheese\":false,\"sub_comic\":false,\"activity\":false,\"series\":false},\"card\":{\"mid\":\"$mid\",\"name\":\"${\n            card.optString(\n                \"name\"\n            )\n        }\",\"approve\":false,\"sex\":\"${card.optString(\"sex\")}\",\"rank\":\"${card.optString(\"rank\")}\",\"face\":\"${\n            card.optString(\n                \"face\"\n            )\n        }\",\"DisplayRank\":\"\",\"regtime\":0,\"spacesta\":0,\"birthday\":\"\",\"place\":\"\",\"description\":\"该页面由哔哩漫游修复\",\"article\":0,\"attentions\":null,\"fans\":${\n            card.optInt(\n                \"fans\",\n                114\n            )\n        },\"friend\":${card.optInt(\"friend\", 514)},\"attention\":${\n            card.optInt(\n                \"attention\",\n                233\n            )\n        },\"sign\":\"【该页面由哔哩漫游修复】${card.optString(\"sign\")}\",\"level_info\":{\"current_level\":${\n            levelInfo.optInt(\n                \"current_level\"\n            )\n        },\"current_min\":${levelInfo.optInt(\"current_min\")},\"current_exp\":${levelInfo.optInt(\"current_exp\")},\"next_exp\":\"${\n            levelInfo.optInt(\n                \"next_exp\"\n            )\n        }\"},\"pendant\":{\"pid\":0,\"name\":\"\",\"image\":\"\",\"expire\":0,\"image_enhance\":\"\",\"image_enhance_frame\":\"\"},\"nameplate\":{\"nid\":0,\"name\":\"\",\"image\":\"\",\"image_small\":\"\",\"level\":\"\",\"condition\":\"\"},\"official_verify\":{\"type\":${\n            officialVerify.optInt(\n                \"type\"\n            )\n        },\"desc\":\"${officialVerify.optString(\"desc\")}\",\"role\":3,\"title\":\"${\n            officialVerify.optString(\n                \"desc\"\n            )\n        }\"},\"vip\":{\"vipType\":0,\"vipDueDate\":0,\"dueRemark\":\"\",\"accessStatus\":0,\"vipStatus\":0,\"vipStatusWarn\":\"\",\"themeType\":0,\"label\":{\"path\":\"\",\"text\":\"\",\"label_theme\":\"\",\"text_color\":\"\",\"bg_style\":0,\"bg_color\":\"\",\"border_color\":\"\"}},\"silence\":0,\"end_time\":0,\"silence_url\":\"\",\"likes\":{\"like_num\":0,\"skr_tip\":\"该页面由哔哩漫游修复\"},\"pr_info\":{},\"relation\":{\"status\":1},\"is_deleted\":0,\"honours\":{\"colour\":{\"dark\":\"#CE8620\",\"normal\":\"#F0900B\"},\"tags\":null},\"profession\":{}},\"images\":{\"imgUrl\":\"https://i0.hdslb.com/bfs/album/16b6731618d911060e26f8fc95684c26bddc897c.jpg\",\"night_imgurl\":\"https://i0.hdslb.com/bfs/album/ca79ebb2ebeee86c5634234c688b410661ea9623.png\",\"has_garb\":true,\"goods_available\":true},\"live\":{\"roomStatus\":0,\"roundStatus\":0,\"liveStatus\":0,\"url\":\"\",\"title\":\"\",\"cover\":\"\",\"online\":0,\"roomid\":0,\"broadcast_type\":0,\"online_hidden\":0,\"link\":\"\"},\"archive\":{\"order\":[{\"title\":\"最新发布\",\"value\":\"pubdate\"},{\"title\":\"最多播放\",\"value\":\"click\"}],\"count\":9999,\"item\":[]},\"series\":{\"item\":[]},\"play_game\":{\"count\":0,\"item\":[]},\"article\":{\"count\":0,\"item\":[],\"lists_count\":0,\"lists\":[]},\"season\":{\"count\":0,\"item\":[]},\"coin_archive\":{\"count\":0,\"item\":[]},\"like_archive\":{\"count\":0,\"item\":[]},\"audios\":{\"count\":0,\"item\":[]},\"favourite2\":{\"count\":0,\"item\":[]},\"comic\":{\"count\":0,\"item\":[]},\"ugc_season\":{\"count\":0,\"item\":[]},\"cheese\":{\"count\":0,\"item\":[]},\"fans_effect\":{},\"tab2\":[{\"title\":\"动态\",\"param\":\"dynamic\"},{\"title\":\"投稿\",\"param\":\"contribute\",\"items\":[{\"title\":\"视频\",\"param\":\"video\"}]}]}\"\"\"\n    }\n\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    fun getContent(urlString: String, mobiApp: String = platform): String? {\n        val timeout = 10000\n        return try {\n            // Work around for android 7\n            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N &&\n                urlString.startsWith(\"https\") &&\n                !urlString.contains(\"bilibili.com\")\n            ) {\n                Log.d(\"Found Android 7, try to bypass ssl issue\")\n                val handler = Handler(AndroidAppHelper.currentApplication().mainLooper)\n                val listener = object : Any() {\n                    val latch = CountDownLatch(1)\n                    var result = \"\"\n\n                    @Suppress(\"UNUSED\")\n                    @JavascriptInterface\n                    fun callback(r: String) {\n                        result = r\n                        latch.countDown()\n                    }\n                }\n                handler.post {\n                    val webView = WebView(currentContext, null)\n                    webView.addJavascriptInterface(listener, \"listener\")\n                    webView.webViewClient = object : WebViewClient() {\n                        override fun onPageFinished(view: WebView?, url: String?) {\n                            view?.settings?.javaScriptEnabled = true\n                            view?.loadUrl(\"javascript:listener.callback(document.documentElement.innerText)\")\n                        }\n                    }\n                    webView.loadUrl(\n                        urlString, mapOf(\n                            \"x-from-biliroaming\" to BuildConfig.VERSION_NAME,\n                            \"platform-from-biliroaming\" to mobiApp,\n                            \"Build\" to BuildConfig.VERSION_CODE.toString()\n                        )\n                    )\n                }\n                try {\n                    if (!listener.latch.await((timeout * 2).toLong(), TimeUnit.MILLISECONDS)) {\n                        Log.toast(\"连接超时，请重试\")\n                        throw IOException(\"Timeout connection to server\")\n                    }\n                } catch (e: InterruptedException) {\n                    throw IOException(\"Connection to server was interrupted\")\n                }\n                return listener.result\n            } else {\n                val url = URL(urlString)\n                val connection = url.openConnection() as HttpURLConnection\n                connection.requestMethod = \"GET\"\n                connection.setRequestProperty(\"Build\", BuildConfig.VERSION_CODE.toString())\n                connection.connectTimeout = timeout\n                connection.readTimeout = timeout\n                connection.setRequestProperty(\"x-from-biliroaming\", BuildConfig.VERSION_NAME)\n                connection.setRequestProperty(\n                    \"Accept-Encoding\",\n                    \"${if (instance.brotliInputStreamClass != null) \"br,\" else \"\"}gzip,deflate\"\n                )\n                connection.setRequestProperty(\"platform-from-biliroaming\", mobiApp)\n                connection.connect()\n                if (connection.responseCode == HttpURLConnection.HTTP_OK) {\n                    val inputStream = connection.inputStream\n                    getStreamContent(\n                        when (connection.contentEncoding?.lowercase()) {\n                            \"gzip\" -> GZIPInputStream(inputStream)\n                            \"br\" -> instance.brotliInputStreamClass!!.new(inputStream) as InputStream\n                            \"deflate\" -> InflaterInputStream(inputStream)\n                            else -> inputStream\n                        }\n                    )\n                } else null\n            }\n\n        } catch (e: Throwable) {\n            Log.e(\"getContent error: $e with url $urlString\")\n            Log.e(e)\n            null\n        }?.also {\n            Log.d(\"getContent url: $urlString mobiApp: $mobiApp\")\n            Log.d(\"getContent result: $it\")\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/Coroutines.kt",
    "content": "package me.iacn.biliroaming.utils\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.json.JSONObject\nimport java.net.URL\n\nsuspend fun fetchJson(url: URL) = withContext(Dispatchers.IO) {\n    try {\n        JSONObject(url.readText())\n    } catch (e: Throwable) {\n        null\n    }\n}\n\n@Suppress(\"BlockingMethodInNonBlockingContext\") // Fuck JetBrain\nsuspend fun fetchJson(url: String) = fetchJson(URL(url))\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/DexHelper.java",
    "content": "package me.iacn.biliroaming.utils;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.Closeable;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Member;\n\npublic class DexHelper implements AutoCloseable, Closeable {\n    public static final int NO_CLASS_INDEX = -1;\n    private final ClassLoader classLoader;\n    private final long token;\n\n    public DexHelper(@NonNull ClassLoader classLoader) {\n        this.classLoader = classLoader;\n        token = load(classLoader);\n    }\n\n    @NonNull\n    public native long[] findMethodUsingString(\n            @NonNull String str, boolean matchPrefix, long returnType, short parameterCount,\n            @Nullable String parameterShorty, long declaringClass, @Nullable long[] parameterTypes,\n            @Nullable long[] containsParameterTypes, @Nullable int[] dexPriority, boolean findFirst);\n\n    @NonNull\n    public native long[] findMethodInvoking(long methodIndex, long returnType,\n                                            short parameterCount, @Nullable String parameterShorty,\n                                            long declaringClass, @Nullable long[] parameterTypes,\n                                            @Nullable long[] containsParameterTypes,\n                                            @Nullable int[] dexPriority, boolean findFirst);\n\n    @NonNull\n    public native long[] findMethodInvoked(long methodIndex, long returnType,\n                                           short parameterCount, @Nullable String parameterShorty,\n                                           long declaringClass, @Nullable long[] parameterTypes,\n                                           @Nullable long[] containsParameterTypes,\n                                           @Nullable int[] dexPriority, boolean findFirst);\n\n    @NonNull\n    public native long[] findMethodSettingField(\n            long fieldIndex, long returnType, short parameterCount,\n            @Nullable String parameterShorty, long declaringClass, @Nullable long[] parameterTypes,\n            @Nullable long[] containsParameterTypes, @Nullable int[] dexPriority, boolean findFirst);\n\n    @NonNull\n    public native long[] findMethodGettingField(\n            long fieldIndex, long returnType, short parameterCount,\n            @Nullable String parameterShorty, long declaringClass, @Nullable long[] parameterTypes,\n            @Nullable long[] containsParameterTypes, @Nullable int[] dexPriority, boolean findFirst);\n\n    @NonNull\n    public native long[] findField(long type, @Nullable int[] dexPriority, boolean findFirst);\n\n    @Nullable\n    public native Member decodeMethodIndex(long methodIndex);\n\n    public native long encodeMethodIndex(@NonNull Member method);\n\n    @Nullable\n    public native Field decodeFieldIndex(long fieldIndex);\n\n    public native long encodeFieldIndex(@NonNull Field field);\n\n    public native long encodeClassIndex(@NonNull Class<?> clazz);\n\n    @Nullable\n    public native Class<?> decodeClassIndex(long classIndex);\n\n    public native void createFullCache();\n\n    @Override\n    public native void close();\n\n    @Override\n    protected void finalize() {\n        close();\n    }\n\n    private native long load(@NonNull ClassLoader classLoader);\n}\n\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/KotlinXposedHelper.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage me.iacn.biliroaming.utils\n\nimport android.content.res.XResources\nimport dalvik.system.BaseDexClassLoader\nimport de.robv.android.xposed.XC_MethodHook\nimport de.robv.android.xposed.XC_MethodHook.MethodHookParam\nimport de.robv.android.xposed.XC_MethodReplacement\nimport de.robv.android.xposed.XposedBridge.*\nimport de.robv.android.xposed.XposedHelpers.*\nimport de.robv.android.xposed.callbacks.XC_LayoutInflated\nimport java.lang.reflect.Field\nimport java.lang.reflect.Member\nimport java.lang.reflect.Modifier\nimport java.util.*\n\ntypealias MethodHookParam = MethodHookParam\ntypealias Replacer = (MethodHookParam) -> Any?\ntypealias Hooker = (MethodHookParam) -> Unit\n\nfun Class<*>.hookMethod(method: String?, vararg args: Any?) = try {\n    findAndHookMethod(this, method, *args)\n} catch (e: NoSuchMethodError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    null\n}\n\nfun Member.hookMethod(callback: XC_MethodHook) = try {\n    hookMethod(this, callback)\n} catch (e: Throwable) {\n    Log.e(e)\n    null\n}\n\ninline fun MethodHookParam.callHooker(crossinline hooker: Hooker) = try {\n    hooker(this)\n} catch (e: Throwable) {\n    Log.e(\"Error occurred calling hooker on ${this.method}\")\n    Log.e(e)\n}\n\ninline fun MethodHookParam.callReplacer(crossinline replacer: Replacer) = try {\n    replacer(this)\n} catch (e: Throwable) {\n    Log.e(\"Error occurred calling replacer on ${this.method}\")\n    Log.e(e)\n    null\n}\n\ninline fun Member.replaceMethod(crossinline replacer: Replacer) =\n    hookMethod(object : XC_MethodReplacement() {\n        override fun replaceHookedMethod(param: MethodHookParam) = param.callReplacer(replacer)\n    })\n\ninline fun Member.hookAfterMethod(crossinline hooker: Hooker) =\n    hookMethod(object : XC_MethodHook() {\n        override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Member.hookBeforeMethod(crossinline hooker: (MethodHookParam) -> Unit) =\n    hookMethod(object : XC_MethodHook() {\n        override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Class<*>.hookBeforeMethod(\n    method: String?,\n    vararg args: Any?,\n    crossinline hooker: Hooker\n) = hookMethod(method, *args, object : XC_MethodHook() {\n    override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n})\n\ninline fun Class<*>.hookAfterMethod(\n    method: String?,\n    vararg args: Any?,\n    crossinline hooker: Hooker\n) = hookMethod(method, *args, object : XC_MethodHook() {\n    override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n})\n\ninline fun Class<*>.replaceMethod(\n    method: String?,\n    vararg args: Any?,\n    crossinline replacer: Replacer\n) = hookMethod(method, *args, object : XC_MethodReplacement() {\n    override fun replaceHookedMethod(param: MethodHookParam) = param.callReplacer(replacer)\n})\n\nfun Class<*>.hookAllMethods(methodName: String?, hooker: XC_MethodHook): Set<XC_MethodHook.Unhook> =\n    try {\n        hookAllMethods(this, methodName, hooker)\n    } catch (e: NoSuchMethodError) {\n        Log.e(e)\n        emptySet()\n    } catch (e: ClassNotFoundError) {\n        Log.e(e)\n        emptySet()\n    } catch (e: ClassNotFoundException) {\n        Log.e(e)\n        emptySet()\n    }\n\ninline fun Class<*>.hookBeforeAllMethods(methodName: String?, crossinline hooker: Hooker) =\n    hookAllMethods(methodName, object : XC_MethodHook() {\n        override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Class<*>.hookAfterAllMethods(methodName: String?, crossinline hooker: Hooker) =\n    hookAllMethods(methodName, object : XC_MethodHook() {\n        override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n\n    })\n\ninline fun Class<*>.replaceAllMethods(methodName: String?, crossinline replacer: Replacer) =\n    hookAllMethods(methodName, object : XC_MethodReplacement() {\n        override fun replaceHookedMethod(param: MethodHookParam) = param.callReplacer(replacer)\n    })\n\nfun Class<*>.hookConstructor(vararg args: Any?) = try {\n    findAndHookConstructor(this, *args)\n} catch (e: NoSuchMethodError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    null\n}\n\ninline fun Class<*>.hookBeforeConstructor(vararg args: Any?, crossinline hooker: Hooker) =\n    hookConstructor(*args, object : XC_MethodHook() {\n        override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Class<*>.hookAfterConstructor(vararg args: Any?, crossinline hooker: Hooker) =\n    hookConstructor(*args, object : XC_MethodHook() {\n        override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Class<*>.replaceConstructor(vararg args: Any?, crossinline hooker: Hooker) =\n    hookConstructor(*args, object : XC_MethodReplacement() {\n        override fun replaceHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\nfun Class<*>.hookAllConstructors(hooker: XC_MethodHook): Set<XC_MethodHook.Unhook> = try {\n    hookAllConstructors(this, hooker)\n} catch (e: NoSuchMethodError) {\n    Log.e(e)\n    emptySet()\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    emptySet()\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    emptySet()\n}\n\ninline fun Class<*>.hookAfterAllConstructors(crossinline hooker: Hooker) =\n    hookAllConstructors(object : XC_MethodHook() {\n        override fun afterHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Class<*>.hookBeforeAllConstructors(crossinline hooker: Hooker) =\n    hookAllConstructors(object : XC_MethodHook() {\n        override fun beforeHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\ninline fun Class<*>.replaceAllConstructors(crossinline hooker: Hooker) =\n    hookAllConstructors(object : XC_MethodReplacement() {\n        override fun replaceHookedMethod(param: MethodHookParam) = param.callHooker(hooker)\n    })\n\nfun String.hookMethod(classLoader: ClassLoader, method: String?, vararg args: Any?) = try {\n    findClass(classLoader).hookMethod(method, *args)\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    null\n}\n\ninline fun String.hookBeforeMethod(\n    classLoader: ClassLoader,\n    method: String?,\n    vararg args: Any?,\n    crossinline hooker: Hooker\n) = try {\n    findClass(classLoader).hookBeforeMethod(method, *args, hooker = hooker)\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    null\n}\n\ninline fun String.hookAfterMethod(\n    classLoader: ClassLoader,\n    method: String?,\n    vararg args: Any?,\n    crossinline hooker: Hooker\n) = try {\n    findClass(classLoader).hookAfterMethod(method, *args, hooker = hooker)\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    null\n}\n\ninline fun String.replaceMethod(\n    classLoader: ClassLoader,\n    method: String?,\n    vararg args: Any?,\n    crossinline replacer: Replacer\n) = try {\n    findClass(classLoader).replaceMethod(method, *args, replacer = replacer)\n} catch (e: ClassNotFoundError) {\n    Log.e(e)\n    null\n} catch (e: ClassNotFoundException) {\n    Log.e(e)\n    null\n}\n\nfun MethodHookParam.invokeOriginalMethod(): Any? = invokeOriginalMethod(method, thisObject, args)\n\ninline fun <T, R> T.runCatchingOrNull(func: T.() -> R?) = try {\n    func()\n} catch (e: Throwable) {\n    null\n}\n\nfun Any.getObjectField(field: String?): Any? = getObjectField(this, field)\n\nfun Any.getObjectFieldOrNull(field: String?): Any? = runCatchingOrNull {\n    getObjectField(this, field)\n}\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Any.getObjectFieldAs(field: String?) = getObjectField(this, field) as T\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Any.getObjectFieldOrNullAs(field: String?) = runCatchingOrNull {\n    getObjectField(this, field) as T\n}\n\nfun Any.getIntField(field: String?) = getIntField(this, field)\n\nfun Any.getIntFieldOrNull(field: String?) = runCatchingOrNull {\n    getIntField(this, field)\n}\n\nfun Any.getLongField(field: String?) = getLongField(this, field)\n\nfun Any.getLongFieldOrNull(field: String?) = runCatchingOrNull {\n    getLongField(this, field)\n}\n\nfun Any.getBooleanFieldOrNull(field: String?) = runCatchingOrNull {\n    getBooleanField(this, field)\n}\n\nfun Any.callMethod(methodName: String?, vararg args: Any?): Any? =\n    callMethod(this, methodName, *args)\n\nfun Any.callMethodOrNull(methodName: String?, vararg args: Any?): Any? = runCatchingOrNull {\n    callMethod(this, methodName, *args)\n}\n\nfun Class<*>.callStaticMethod(methodName: String?, vararg args: Any?): Any? =\n    callStaticMethod(this, methodName, *args)\n\nfun Class<*>.callStaticMethodOrNull(methodName: String?, vararg args: Any?): Any? =\n    runCatchingOrNull {\n        callStaticMethod(this, methodName, *args)\n    }\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Class<*>.callStaticMethodAs(methodName: String?, vararg args: Any?) =\n    callStaticMethod(this, methodName, *args) as T\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Class<*>.callStaticMethodOrNullAs(methodName: String?, vararg args: Any?) =\n    runCatchingOrNull {\n        callStaticMethod(this, methodName, *args) as T\n    }\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Class<*>.getStaticObjectFieldAs(field: String?) = getStaticObjectField(this, field) as T\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Class<*>.getStaticObjectFieldOrNullAs(field: String?) = runCatchingOrNull {\n    getStaticObjectField(this, field) as T\n}\n\nfun Class<*>.getStaticObjectField(field: String?): Any? = getStaticObjectField(this, field)\n\nfun Class<*>.getStaticObjectFieldOrNull(field: String?): Any? = runCatchingOrNull {\n    getStaticObjectField(this, field)\n}\n\nfun Class<*>.setStaticObjectField(field: String?, obj: Any?) = apply {\n    setStaticObjectField(this, field, obj)\n}\n\nfun Class<*>.setStaticObjectFieldIfExist(field: String?, obj: Any?) = apply {\n    try {\n        setStaticObjectField(this, field, obj)\n    } catch (ignored: Throwable) {\n    }\n}\n\ninline fun <reified T> Class<*>.findFieldByExactType(): Field? =\n    findFirstFieldByExactType(this, T::class.java)\n\nfun Class<*>.findFieldByExactType(type: Class<*>): Field? =\n    findFirstFieldByExactType(this, type)\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Any.callMethodAs(methodName: String?, vararg args: Any?) =\n    callMethod(this, methodName, *args) as T\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Any.callMethodOrNullAs(methodName: String?, vararg args: Any?) = runCatchingOrNull {\n    callMethod(this, methodName, *args) as T\n}\n\nfun Any.callMethod(methodName: String?, parameterTypes: Array<Class<*>>, vararg args: Any?): Any? =\n    callMethod(this, methodName, parameterTypes, *args)\n\nfun Any.callMethodOrNull(\n    methodName: String?,\n    parameterTypes: Array<Class<*>>,\n    vararg args: Any?\n): Any? = runCatchingOrNull {\n    callMethod(this, methodName, parameterTypes, *args)\n}\n\nfun Class<*>.callStaticMethod(\n    methodName: String?,\n    parameterTypes: Array<Class<*>>,\n    vararg args: Any?\n): Any? = callStaticMethod(this, methodName, parameterTypes, *args)\n\nfun Class<*>.callStaticMethodOrNull(\n    methodName: String?,\n    parameterTypes: Array<Class<*>>,\n    vararg args: Any?\n): Any? = runCatchingOrNull {\n    callStaticMethod(this, methodName, parameterTypes, *args)\n}\n\nfun String.findClass(classLoader: ClassLoader?): Class<*> = findClass(this, classLoader)\n\ninfix fun String.on(classLoader: ClassLoader?): Class<*> = findClass(this, classLoader)\n\nfun String.findClassOrNull(classLoader: ClassLoader?): Class<*>? =\n    findClassIfExists(this, classLoader)\n\ninfix fun String.from(classLoader: ClassLoader?): Class<*>? =\n    findClassIfExists(this, classLoader)\n\nfun Class<*>.new(vararg args: Any?): Any = newInstance(this, *args)\n\nfun Class<*>.new(parameterTypes: Array<Class<*>>, vararg args: Any?): Any =\n    newInstance(this, parameterTypes, *args)\n\nfun Class<*>.findField(field: String?): Field = findField(this, field)\n\nfun Class<*>.findFieldOrNull(field: String?): Field? = findFieldIfExists(this, field)\n\nfun <T> T.setIntField(field: String?, value: Int) = apply {\n    setIntField(this, field, value)\n}\n\nfun <T> T.setLongField(field: String?, value: Long) = apply {\n    setLongField(this, field, value)\n}\n\nfun <T> T.setObjectField(field: String?, value: Any?) = apply {\n    setObjectField(this, field, value)\n}\n\nfun <T> T.setBooleanField(field: String?, value: Boolean) = apply {\n    setBooleanField(this, field, value)\n}\n\nfun <T> T.setFloatField(field: String?, value: Float) = apply {\n    setFloatField(this, field, value)\n}\n\ninline fun XResources.hookLayout(\n    id: Int,\n    crossinline hooker: (XC_LayoutInflated.LayoutInflatedParam) -> Unit\n) {\n    try {\n        hookLayout(id, object : XC_LayoutInflated() {\n            override fun handleLayoutInflated(liparam: LayoutInflatedParam) {\n                try {\n                    hooker(liparam)\n                } catch (e: Throwable) {\n                    Log.e(e)\n                }\n            }\n        })\n    } catch (e: Throwable) {\n        Log.e(e)\n    }\n}\n\ninline fun XResources.hookLayout(\n    pkg: String,\n    type: String,\n    name: String,\n    crossinline hooker: (XC_LayoutInflated.LayoutInflatedParam) -> Unit\n) {\n    try {\n        val id = getIdentifier(name, type, pkg)\n        hookLayout(id, hooker)\n    } catch (e: Throwable) {\n        Log.e(e)\n    }\n}\n\nfun Class<*>.findFirstFieldByExactType(type: Class<*>): Field =\n    findFirstFieldByExactType(this, type)\n\nfun Class<*>.findFirstFieldByExactTypeOrNull(type: Class<*>?): Field? = runCatchingOrNull {\n    findFirstFieldByExactType(this, type)\n}\n\nfun Any.getFirstFieldByExactType(type: Class<*>): Any? =\n    javaClass.findFirstFieldByExactType(type).get(this)\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Any.getFirstFieldByExactTypeAs(type: Class<*>) =\n    javaClass.findFirstFieldByExactType(type).get(this) as? T\n\ninline fun <reified T : Any> Any.getFirstFieldByExactType() =\n    javaClass.findFirstFieldByExactType(T::class.java).get(this) as? T\n\nfun Any.getFirstFieldByExactTypeOrNull(type: Class<*>?): Any? = runCatchingOrNull {\n    javaClass.findFirstFieldByExactTypeOrNull(type)?.get(this)\n}\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> Any.getFirstFieldByExactTypeOrNullAs(type: Class<*>?) =\n    getFirstFieldByExactTypeOrNull(type) as? T\n\ninline fun <reified T> Any.getFirstFieldByExactTypeOrNull() =\n    getFirstFieldByExactTypeOrNull(T::class.java) as? T\n\ninline fun ClassLoader.findDexClassLoader(crossinline delegator: (BaseDexClassLoader) -> BaseDexClassLoader = { x -> x }): BaseDexClassLoader? {\n    var classLoader = this\n    while (classLoader !is BaseDexClassLoader) {\n        if (classLoader.parent != null) classLoader = classLoader.parent\n        else return null\n    }\n    return delegator(classLoader)\n}\n\ninline fun ClassLoader.allClassesList(crossinline delegator: (BaseDexClassLoader) -> BaseDexClassLoader = { x -> x }): List<String> {\n    return findDexClassLoader(delegator)?.getObjectField(\"pathList\")\n        ?.getObjectFieldAs<Array<Any>>(\"dexElements\")\n        ?.flatMap {\n            it.getObjectField(\"dexFile\")?.callMethodAs<Enumeration<String>>(\"entries\")?.toList()\n                .orEmpty()\n        }.orEmpty()\n}\n\nval Member.isStatic: Boolean\n    inline get() = Modifier.isStatic(modifiers)\nval Member.isFinal: Boolean\n    inline get() = Modifier.isFinal(modifiers)\nval Member.isPublic: Boolean\n    inline get() = Modifier.isPublic(modifiers)\nval Member.isNotStatic: Boolean\n    inline get() = !isStatic\nval Member.isAbstract: Boolean\n    inline get() = Modifier.isAbstract(modifiers)\nval Member.isPrivate: Boolean\n    inline get() = Modifier.isPrivate(modifiers)\nval Class<*>.isAbstract: Boolean\n    inline get() = !isPrimitive && Modifier.isAbstract(modifiers)\nval Class<*>.isFinal: Boolean\n    inline get() = !isPrimitive && Modifier.isFinal(modifiers)\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/Log.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage me.iacn.biliroaming.utils\n\nimport android.os.Handler\nimport android.os.Looper\nimport android.widget.Toast\nimport de.robv.android.xposed.XposedBridge\nimport me.iacn.biliroaming.BiliBiliPackage\nimport me.iacn.biliroaming.Constant.TAG\nimport android.util.Log as ALog\n\nobject Log {\n\n    private val handler by lazy { Handler(Looper.getMainLooper()) }\n    private var toast: Toast? = null\n\n    fun toast(msg: String, force: Boolean = false, duration: Int = Toast.LENGTH_SHORT, alsoLog: Boolean = true) {\n        if (!force && !sPrefs.getBoolean(\"show_info\", true)) return\n        handler.post {\n            BiliBiliPackage.instance.toastHelperClass?.runCatchingOrNull {\n                callStaticMethod(BiliBiliPackage.instance.cancelShowToast())\n                callStaticMethod(\n                    BiliBiliPackage.instance.showToast(),\n                    currentContext,\n                    \"哔哩漫游：$msg\",\n                    duration\n                )\n                Unit\n            } ?: run {\n                toast?.cancel()\n                toast = Toast.makeText(currentContext, \"\", duration).apply {\n                    setText(\"哔哩漫游：$msg\")\n                    show()\n                }\n            }\n        }\n        if (alsoLog) w(msg)\n    }\n\n    @JvmStatic\n    private fun doLog(f: (String, String) -> Int, obj: Any?, toXposed: Boolean = false) {\n        val str = if (obj is Throwable) ALog.getStackTraceString(obj) else obj.toString()\n\n        if (str.length > maxLength) {\n            val chunkCount: Int = str.length / maxLength\n            for (i in 0..chunkCount) {\n                val max: Int = maxLength * (i + 1)\n                if (max >= str.length) {\n                    doLog(f, str.substring(maxLength * i))\n                } else {\n                    doLog(f, str.substring(maxLength * i, max))\n                }\n            }\n        } else {\n            f(TAG, str)\n            if (toXposed)\n                XposedBridge.log(\"$TAG : $str\")\n        }\n    }\n\n    @JvmStatic\n    fun d(obj: Any?) {\n        doLog(ALog::d, obj)\n    }\n\n    @JvmStatic\n    fun i(obj: Any?) {\n        doLog(ALog::i, obj)\n    }\n\n    @JvmStatic\n    fun e(obj: Any?) {\n        doLog(ALog::e, obj, true)\n    }\n\n    @JvmStatic\n    fun v(obj: Any?) {\n        doLog(ALog::v, obj)\n    }\n\n    @JvmStatic\n    fun w(obj: Any?) {\n        doLog(ALog::w, obj)\n    }\n\n    private const val maxLength = 3000\n}\n\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/StrokeSpan.kt",
    "content": "package me.iacn.biliroaming.utils\n\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.text.TextPaint\nimport android.text.style.ReplacementSpan\n\nclass StrokeSpan(\n    private val fillColor: Int,\n    private val strokeColor: Int,\n    private val strokeWidth: Float\n) : ReplacementSpan() {\n    private fun fillPaint(paint: Paint): TextPaint =\n        TextPaint(paint).apply {\n            style = Paint.Style.FILL\n            color = fillColor\n        }\n\n    private fun stokePaint(paint: Paint): TextPaint =\n        TextPaint(paint).apply {\n            style = Paint.Style.STROKE\n            color = strokeColor\n            strokeWidth = this@StrokeSpan.strokeWidth\n        }\n\n    override fun getSize(\n        p0: Paint,\n        p1: CharSequence?,\n        p2: Int,\n        p3: Int,\n        p4: Paint.FontMetricsInt?\n    ): Int {\n        return p0.measureText(p1, p2, p3).toInt()\n    }\n\n    override fun draw(\n        canvas: Canvas,\n        text: CharSequence?,\n        start: Int,\n        end: Int,\n        x: Float,\n        top: Int,\n        y: Int,\n        bottom: Int,\n        paint: Paint\n    ) {\n        text ?: return\n        canvas.drawText(text, start, end, x, y.toFloat(), fillPaint(paint))\n        if (strokeWidth > 0)\n            canvas.drawText(text, start, end, x, y.toFloat(), stokePaint(paint))\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/SubtitleHelper.kt",
    "content": "package me.iacn.biliroaming.utils\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.BitmapFactory.Options\nimport me.iacn.biliroaming.R\nimport me.iacn.biliroaming.XposedInit.Companion.moduleRes\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.io.*\nimport java.net.URL\nimport java.nio.ByteBuffer\nimport java.util.zip.GZIPInputStream\n\nclass TrieNode<V>(val key: Char, val level: Int = 0) {\n    private val children = hashMapOf<Char, TrieNode<V>>()\n\n    val isLeaf get() = value != null\n    var value: V? = null\n\n    fun getOrAddChild(k: Char) = children.computeIfAbsent(k) { TrieNode(k, level + 1) }\n\n    fun child(k: Char) = children[k]\n}\n\nclass Trie<T> {\n    private val root = TrieNode<T>(key = '\\u0000')\n\n    fun add(w: String, value: T) {\n        if (w.isEmpty()) return\n        var p = root\n        for (c in w.toCharArray())\n            p = p.getOrAddChild(c)\n        p.value = value\n    }\n\n    fun bestMatch(sen: CharArray): TrieNode<T>? {\n        var node: TrieNode<T> = root\n        var leaf: TrieNode<T>? = null\n        for (c in sen) {\n            node = node.child(c) ?: break\n            if (node.isLeaf) leaf = node\n        }\n        return leaf\n    }\n}\n\nclass Dictionary(\n    private val chars: Map<Char, Char>,\n    private val dict: Trie<String>,\n    private val maxLen: Int\n) {\n    private fun convert(reader: Reader, writer: Writer) {\n        val `in` = PushbackReader(reader.buffered(), maxLen)\n        val buf = CharArray(maxLen)\n        var len: Int\n\n        while (true) {\n            len = `in`.read(buf)\n            if (len == -1) break\n            val node = dict.bestMatch(buf)\n            if (node != null) {\n                val offset = node.level\n                node.value?.let { writer.write(it) }\n                `in`.unread(buf, offset, len - offset)\n            } else {\n                `in`.unread(buf, 0, len)\n                val ch = `in`.read().toChar()\n                writer.write(chars.getOrDefault(ch, ch).code)\n            }\n        }\n    }\n\n    fun convert(str: String) = StringWriter().also {\n        convert(str.reader(), it)\n    }.toString()\n\n    companion object {\n        private const val SHARP = '#'\n        private const val EQUAL = '='\n\n        fun loadDictionary(mappingFile: File): Dictionary {\n            val charMap = HashMap<Char, Char>(4096)\n            val dict = Trie<String>()\n            var maxLen = 2\n            mappingFile.bufferedReader().useLines { lines ->\n                lines.filterNot { it.isBlank() || it.trimStart().startsWith(SHARP) }\n                    .map { it.split(EQUAL, limit = 2) }.filter { it.size == 2 }.forEach { (k, v) ->\n                        if (k.length == 1 && v.length == 1) {\n                            charMap[k[0]] = v[0]\n                        } else {\n                            maxLen = k.length.coerceAtLeast(maxLen)\n                            dict.add(k, v)\n                        }\n                    }\n            }\n            return Dictionary(charMap, dict, maxLen)\n        }\n    }\n}\n\nobject SubtitleHelper {\n    private val dictFile by lazy { File(currentContext.filesDir, \"t2cn.txt\") }\n    private val dictionary by lazy { Dictionary.loadDictionary(dictFile) }\n    private const val dictUrl =\n        \"https://archive.biliimg.com/bfs/archive/566adec17e127bf92aed21832db0206ccecc8caa.png\"\n\n    // !!! Do not remove symbol '\\' for \"\\}\", Android need it\n    @Suppress(\"RegExpRedundantEscape\")\n    private val noStyleRegex =\n        Regex(\"\"\"\\{\\\\?\\\\an\\d+\\}|<font\\s[^>]*>|<\\\\?/font>|<i>|<\\\\?/i>|<b>|<\\\\?/b>|<u>|<\\\\?/u>\"\"\")\n    val dictExist get() = dictFile.isFile\n\n    @Synchronized\n    fun downloadDict(): Boolean {\n        if (dictExist) return true\n        runCatching {\n            val buffer = URL(dictUrl).openStream().buffered().use {\n                val options = Options().apply { inPreferredConfig = Bitmap.Config.RGB_565 }\n                val bitmap = BitmapFactory.decodeStream(it, null, options)\n                ByteBuffer.allocate(bitmap!!.byteCount).apply {\n                    bitmap.let { b -> b.copyPixelsToBuffer(this); b.recycle() }\n                    rewind()\n                }\n            }\n            val bytes = ByteArray(buffer.int).also { buffer.get(it) }\n            dictFile.outputStream().use { o ->\n                GZIPInputStream(bytes.inputStream()).use { it.copyTo(o) }\n            }\n        }.onSuccess {\n            return true\n        }.onFailure {\n            Log.e(it)\n            dictFile.delete()\n        }\n        return false\n    }\n\n    fun convert(json: String): String {\n        val subJson = JSONObject(json)\n        var subBody = subJson.optJSONArray(\"body\") ?: return json\n        val subText = subBody.asSequence<JSONObject>().map { it.optString(\"content\") }\n            .joinToString(\"\\u0000\").run {\n                // Remove srt style, bilibili not support it\n                if (contains(\"\\\\an\") || contains(\"<font\")\n                    || contains(\"<i>\") || contains(\"<b>\") || contains(\"<u>\")\n                ) replace(noStyleRegex, \"\") else this\n            }\n        val converted = dictionary.convert(subText)\n        val lines = converted.split('\\u0000')\n        subBody.asSequence<JSONObject>().zip(lines.asSequence()).forEach { (obj, line) ->\n            obj.put(\"content\", line)\n        }\n        subBody = subBody.appendInfo(moduleRes.getString(R.string.subtitle_append_info))\n        return subJson.apply {\n            put(\"body\", subBody)\n        }.toString()\n    }\n\n    fun errorResponse(content: String) = JSONObject().apply {\n        put(\"body\", JSONArray().apply {\n            put(JSONObject().apply {\n                put(\"from\", 0)\n                put(\"location\", 2)\n                put(\"to\", 9999)\n                put(\"content\", content)\n            })\n        })\n    }.toString()\n\n    private fun JSONArray.appendInfo(content: String): JSONArray {\n        if (length() == 0) return this\n        val firstLine = optJSONObject(0)\n            ?: return this\n        val lastLine = optJSONObject(length() - 1)\n            ?: return this\n        val firstFrom = firstLine.optDouble(\"from\")\n            .takeIf { !it.isNaN() } ?: return this\n        val lastTo = lastLine.optDouble(\"to\")\n            .takeIf { !it.isNaN() } ?: return this\n        val minDuration = 1.0\n        val maxDuration = 5.0\n        val interval = 0.3\n        val appendStart = firstFrom >= minDuration + interval\n        val from = if (appendStart) 0.0 else lastTo + interval\n        val to = if (appendStart) {\n            from + (firstFrom - interval).coerceAtMost(maxDuration)\n        } else from + maxDuration\n        val info = JSONObject().apply {\n            put(\"from\", from)\n            put(\"location\", 2)\n            put(\"to\", to)\n            put(\"content\", content)\n        }\n        return if (appendStart) {\n            JSONArray().apply {\n                put(info)\n                for (jo in this@appendInfo) {\n                    put(jo)\n                }\n            }\n        } else apply { put(info) }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/UposReplaceHelper.kt",
    "content": "package me.iacn.biliroaming.utils\n\nimport android.net.Uri\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.future.future\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.R\nimport me.iacn.biliroaming.UGCPlayViewReply\nimport me.iacn.biliroaming.XposedInit\nimport java.util.concurrent.CompletableFuture\nimport java.util.concurrent.TimeUnit\n\nobject UposReplaceHelper {\n    private val aliHost = string(R.string.ali_host)\n    private val cosHost = string(R.string.cos_host)\n    private val hwHost = string(R.string.hw_host)\n    private val aliovHost = string(R.string.aliov_host)\n    private val hwovHost = string(R.string.hwov_host)\n    private val hkBcacheHost = string(R.string.hk_bcache_host)\n\n    val isLocatedCn by lazy {\n        (runCatchingOrNull { XposedInit.country.get(5L, TimeUnit.SECONDS) } ?: \"cn\") == \"cn\"\n    }\n\n    val forceUpos = sPrefs.getBoolean(\"force_upos\", false)\n    val enablePcdnBlock = sPrefs.getBoolean(\"block_pcdn\", false)\n    val enableLivePcdnBlock = sPrefs.getBoolean(\"block_pcdn_live\", false)\n\n    private lateinit var videoUposList: CompletableFuture<List<String>>\n    private val mainVideoUpos =\n        sPrefs.getString(\"upos_host\", null) ?: if (isLocatedCn) hwHost else aliovHost\n    private val serverList = XposedInit.moduleRes.getStringArray(R.array.upos_values)\n    private val extraVideoUposList = when (serverList.indexOf(mainVideoUpos)) {\n        in 1..3 -> listOf(hwHost, cosHost)\n        in 5..7 -> listOf(hwHost, aliHost)\n        in 8..15 -> listOf(aliHost, cosHost)\n        else -> listOf(aliHost, hkBcacheHost)\n    }\n    private val videoUposBase by lazy {\n        runCatchingOrNull { videoUposList.get(500L, TimeUnit.MILLISECONDS) }?.get(0)\n            ?: mainVideoUpos\n    }\n    val videoUposBackups by lazy {\n        runCatchingOrNull { videoUposList.get(500L, TimeUnit.MILLISECONDS) }?.subList(1, 3)\n            ?: extraVideoUposList\n    }\n    const val liveUpos = \"c1--cn-gotcha01.bilivideo.com\"\n\n    val enableUposReplace = (mainVideoUpos != \"\\$1\")\n\n    private val overseaVideoUposRegex by lazy {\n        Regex(\"\"\"(akamai|(ali|hw|cos)\\w*ov|hk-eq-bcache|bstar1)\"\"\")\n    }\n    private val urlBwRegex by lazy { Regex(\"\"\"(bw=[^&]*)\"\"\") }\n    private val ipPCdnRegex by lazy { Regex(\"\"\"^https?://\\d{1,3}\\.\\d{1,3}\"\"\") }\n    val gotchaRegex by lazy { Regex(\"\"\"https?://\\w*--\\w*-gotcha\\d*\\.bilivideo\"\"\") }\n\n    fun initVideoUposList(mClassLoader: ClassLoader) {\n        videoUposList = MainScope().future(Dispatchers.IO) {\n            val bCacheRegex = Regex(\"\"\"cn-.*\\.bilivideo\"\"\")\n            mutableListOf(mainVideoUpos).apply {\n                // 8K video sample, without area limitation, reply probably contains Mirror CDN\n                val playViewReply = instance.playViewReqClass?.new()?.apply {\n                    callMethod(\"setAid\", 355749246L)\n                    callMethod(\"setCid\", 1115447032L)\n                    callMethod(\"setQn\", 127)\n                    callMethod(\"setFnval\", 4048)\n                    callMethod(\"setFourk\", true)\n                    callMethod(\"setForceHost\", 2)\n                }?.let { playViewReqUgc ->\n                    instance.playURLMossClass?.new()?.callMethod(\"playView\", playViewReqUgc)\n                        ?.callMethodAs<ByteArray>(\"toByteArray\")?.let {\n                            UGCPlayViewReply.parseFrom(it)\n                        }\n                }\n                val officialList = playViewReply?.videoInfo?.let { info ->\n                    mutableListOf<String>().apply {\n                        info.streamListList?.forEach { stream ->\n                            add(stream.dashVideo.baseUrl)\n                            addAll(stream.dashVideo.backupUrlList)\n                        }\n                        info.dashAudioList?.forEach { dashItem ->\n                            add(dashItem.baseUrl)\n                            addAll(dashItem.backupUrlList)\n                        }\n                    }\n                }?.mapNotNull { Uri.parse(it).encodedAuthority }?.distinct()\n                    ?.filter { !it.isPCdnUpos() }.orEmpty()\n                addAll(officialList.filter { !(it.contains(bCacheRegex) || it == mainVideoUpos) }\n                    .ifEmpty { officialList })\n                addAll(extraVideoUposList)\n            }.also { hookTf(mClassLoader) }\n        }\n    }\n\n    fun String.isPCdnUpos() =\n        contains(\"szbdyd.com\") || contains(\".mcdn.bilivideo\") || contains(ipPCdnRegex)\n\n    fun String.isOverseaUpos() = isLocatedCn == contains(overseaVideoUposRegex)\n\n    fun String.isNeedReplaceVideoUpos() =\n        if (contains(\".mcdn.bilivideo\") || contains(ipPCdnRegex)) {\n            // IP:Port type PCDN currently only exists in Live and Thai Video.\n            // Cannot simply replace IP:Port or 'mcdn.bilivideo' like PCDN's host\n            false\n        } else {\n            // only 'szbdyd.com' like PCDN can be replace\n            (forceUpos && startsWith(\"http\")) || (enablePcdnBlock && contains(\"szbdyd.com\")) || isOverseaUpos()\n        }\n\n    fun String.replaceUpos(\n        upos: String = videoUposBase, needReplace: Boolean = true\n    ): String {\n        fun String.replaceUposBw(): String = replace(urlBwRegex, \"bw=1280000\")\n\n        return if (needReplace) {\n            val uri = Uri.parse(this)\n            val newUpos = uri.getQueryParameter(\"xy_usource\") ?: upos\n            uri.replaceUpos(newUpos).toString().replaceUposBw()\n        } else replaceUposBw()\n    }\n\n    fun Uri.replaceUpos(upos: String): Uri = buildUpon().authority(upos).build()\n\n    private fun hookTf(mClassLoader: ClassLoader) {\n        if (!(enablePcdnBlock || forceUpos)) return\n        // fake grpc TF header then only reply with mirror type playurl\n        \"com.bilibili.lib.moss.utils.RuntimeHelper\".from(mClassLoader)\n            ?.hookAfterMethod(\"tf\") { param ->\n                val result = param.result\n                if (result.callMethodOrNullAs<Int>(\"getNumber\") != 0) return@hookAfterMethod\n                result.javaClass.callStaticMethodOrNull(\"forNumber\", 1)?.let {\n                    param.result = it\n                }\n            }\n    }\n\n    private fun string(resId: Int) = XposedInit.moduleRes.getString(resId)\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/Utils.kt",
    "content": "package me.iacn.biliroaming.utils\n\nimport android.annotation.SuppressLint\nimport android.app.AndroidAppHelper\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.content.pm.PackageManager.GET_META_DATA\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport android.util.TypedValue\nimport android.view.*\nimport android.widget.ListView\nimport androidx.annotation.LayoutRes\nimport androidx.annotation.RequiresApi\nimport androidx.documentfile.provider.DocumentFile\nimport com.google.protobuf.GeneratedMessageLite\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.Constant\nimport me.iacn.biliroaming.XposedInit\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.io.File\nimport java.io.IOException\nimport java.io.InputStream\nimport java.lang.ref.WeakReference\nimport java.lang.reflect.Field\nimport java.lang.reflect.Proxy\nimport java.math.BigInteger\nimport java.net.URL\nimport java.util.*\nimport java.util.function.Consumer\nimport kotlin.math.roundToInt\nimport kotlin.reflect.KProperty\n\nclass Weak<T>(val initializer: () -> T?) {\n    private var weakReference: WeakReference<T?>? = null\n\n    operator fun getValue(thisRef: Any?, property: KProperty<*>) = weakReference?.get() ?: let {\n        weakReference = WeakReference(initializer())\n        weakReference\n    }?.get()\n\n    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {\n        weakReference = WeakReference(value)\n    }\n}\n\nval systemContext: Context\n    get() {\n        val activityThread = \"android.app.ActivityThread\".findClassOrNull(null)\n            ?.callStaticMethod(\"currentActivityThread\")!!\n        return activityThread.callMethodAs(\"getSystemContext\")\n    }\n\n// https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/bvid_desc.html#bv-av%E7%AE%97%E6%B3%95\nfun bv2av(bv: String): Long {\n    val table = HashMap<Char, Int>()\n    \"FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf\".forEachIndexed { i, b ->\n        table[b] = i\n    }\n    val r = intArrayOf(11, 10, 3, 8, 4, 6, 5, 7, 9).withIndex().sumOf { (i, p) ->\n        table[bv[p]]!! * BigInteger.valueOf(58).pow(i).toLong()\n    }\n    return r.and(2251799813685247L).xor(23442827791579L)\n}\n\nfun getPackageVersion(packageName: String) = try {\n    @Suppress(\"DEPRECATION\")\n    systemContext.packageManager.getPackageInfo(packageName, 0).run {\n        String.format(\"${packageName}@%s(%s)\", versionName, getVersionCode(packageName))\n    }\n} catch (e: Throwable) {\n    Log.e(e)\n    \"(unknown)\"\n}\n\n\nfun getVersionCode(packageName: String) = try {\n    @Suppress(\"DEPRECATION\")\n    systemContext.packageManager.getPackageInfo(packageName, 0).versionCode\n} catch (e: Throwable) {\n    Log.e(e)\n    null\n} ?: 6080000\n\n\nval appKeyMap = mapOf(\n    \"tv.danmaku.bili\" to \"1d8b6e7d45233436\",\n    \"com.bilibili.app.blue\" to \"07da50c9a0bf829f\",\n    \"com.bilibili.app.in\" to \"bb3101000e232e27\",\n    \"tv.danmaku.bilibilihd\" to \"dfca71928277209b\",\n)\n\nval currentContext by lazy { AndroidAppHelper.currentApplication() as Context }\n\nval packageName: String by lazy { currentContext.packageName }\n\nval isBuiltIn\n    get() = XposedInit.modulePath.endsWith(\"so\")\n\nval is64\n    get() = currentContext.applicationInfo.nativeLibraryDir.contains(\"64\")\n\nval platform by lazy {\n    @Suppress(\"DEPRECATION\")\n    currentContext.packageManager.getApplicationInfo(packageName, GET_META_DATA).metaData.getString(\n        \"MOBI_APP\"\n    )\n        ?: when (packageName) {\n            Constant.BLUE_PACKAGE_NAME -> \"android_b\"\n            Constant.PLAY_PACKAGE_NAME -> \"android_i\"\n            Constant.HD_PACKAGE_NAME -> \"android_hd\"\n            else -> \"android\"\n        }\n}\n\nval logFile by lazy { File(currentContext.externalCacheDir, \"log.txt\") }\n\nval oldLogFile by lazy { File(currentContext.externalCacheDir, \"old_log.txt\") }\n\n@Suppress(\"DEPRECATION\")\nval sPrefs: SharedPreferences\n    get() = currentContext.getSharedPreferences(\"biliroaming\", Context.MODE_MULTI_PROCESS)\n\n@Suppress(\"DEPRECATION\")\nval sCaches: SharedPreferences\n    get() = currentContext.getSharedPreferences(\"biliroaming_cache\", Context.MODE_MULTI_PROCESS)\n\n@Suppress(\"DEPRECATION\")\nval biliPrefs: SharedPreferences\n    get() = currentContext.getSharedPreferences(\"bili_preference\", Context.MODE_MULTI_PROCESS)\n\nval blkvPrefs: SharedPreferences\n    get() = instance.biliGlobalPreferenceClass?.callStaticMethodAs(instance.getBLKVPrefs())\n        ?: biliPrefs\n\nfun checkErrorToast(json: JSONObject, isCustomServer: Boolean = false) {\n    if (json.optInt(\"code\", 0) != 0) {\n        Log.toast(\n            (if (isCustomServer) \"请求解析服务器发生错误: \" else \"请求发生错误: \") + json.optString(\n                \"message\",\n                \"未知错误\"\n            )\n        )\n    }\n}\n\nfun signQuery(query: String?, extraMap: Map<String, String> = emptyMap()): String? {\n    query ?: return null\n    val queryMap = query.split('&')\n        .map { it.split('=', limit = 2) }\n        .filter { it.size == 2 }\n        .associate { Pair(it[0], it[1]) }\n    return signQuery(queryMap, extraMap)\n}\n\nfun signQuery(query: Map<String, String>, extraMap: Map<String, String> = emptyMap()): String {\n    val queryMap = TreeMap<String, String>()\n    queryMap.putAll(query)\n    val packageName = AndroidAppHelper.currentPackageName()\n    queryMap[\"appkey\"] = instance.appKey\n    queryMap[\"build\"] = getVersionCode(packageName).toString()\n    queryMap[\"device\"] = \"android\"\n    queryMap[\"mobi_app\"] = platform\n    queryMap[\"platform\"] = \"android\"\n    queryMap.putAll(extraMap)\n    queryMap.remove(\"ts\")\n    queryMap.remove(\"sign\")\n    return instance.libBiliClass?.callStaticMethod(instance.signQueryName(), queryMap).toString()\n}\n\n@SuppressLint(\"DiscouragedApi\")\nfun getId(name: String) = instance.ids[name]\n    ?: currentContext.resources.getIdentifier(name, \"id\", currentContext.packageName)\n\n@SuppressLint(\"DiscouragedApi\")\nfun getResId(name: String, type: String) =\n    currentContext.resources.getIdentifier(name, type, currentContext.packageName)\n\nfun getBitmapFromURL(src: String?, callback: (Bitmap?) -> Unit) {\n    Thread {\n        callback(try {\n            src?.let {\n                val bytes = URL(it).readBytes()\n                BitmapFactory.decodeByteArray(bytes, 0, bytes.size)\n            }\n        } catch (e: IOException) {\n            Log.e(e)\n            null\n        })\n    }.start()\n}\n\nfun String?.toJSONObject() = JSONObject(this.orEmpty())\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <T> JSONArray.asSequence() = (0 until length()).asSequence().map { get(it) as T }\n\noperator fun JSONArray.iterator(): Iterator<JSONObject> =\n    (0 until length()).asSequence().map { get(it) as JSONObject }.iterator()\n\nfun JSONArray?.orEmpty() = this ?: JSONArray()\n\nfun getStreamContent(input: InputStream) = try {\n    input.bufferedReader().use {\n        it.readText()\n    }\n} catch (e: Throwable) {\n    Log.e(e)\n    null\n}\n\n/**\n * @param targetDir 目标文件夹\n */\nfun DocumentFile.copyTo(targetDir: DocumentFile) {\n    val name = name ?: return\n    if (isDirectory) {\n        val chile = targetDir.findOrCreateDir(name) ?: return\n        listFiles().forEach {\n            it.copyTo(chile)\n        }\n    } else if (isFile) {\n        val type = type ?: return\n        val targetFile = targetDir.createFile(type, name) ?: return\n        currentContext.contentResolver.openInputStream(uri)?.use {\n            currentContext.contentResolver.openOutputStream(targetFile.uri)\n                ?.use { o -> it.copyTo(o) }\n        }\n    }\n}\n\nfun DocumentFile.findOrCreateDir(displayName: String) = this.findFile(displayName)\n    ?: this.createDirectory(displayName)\n\nval shouldSaveLog\n    get() = sPrefs.getBoolean(\"save_log\", false) || sPrefs.getBoolean(\"show_hint\", true)\n\nfun GeneratedMessageLite<*, *>.print(indent: Int = 0): String {\n    val sb = StringBuilder()\n    for (f in javaClass.declaredFields) {\n        if (f.name.startsWith(\"bitField\")) continue\n        if (f.isStatic) continue\n        f.isAccessible = true\n        val v = f.get(this)\n        val name = StringBuffer().run {\n            for (i in 0 until indent) append('\\t')\n            append(f.name.substringBeforeLast(\"_\"), \": \")\n            toString()\n        }\n        when (v) {\n            is GeneratedMessageLite<*, *> -> {\n                sb.appendLine(name)\n                sb.append(v.print(indent + 1))\n            }\n\n            is List<*> -> {\n                for (vv in v) {\n                    sb.append(name)\n                    when (vv) {\n                        is GeneratedMessageLite<*, *> -> {\n                            sb.appendLine()\n                            sb.append(vv.print(indent + 1))\n                        }\n\n                        else -> {\n                            sb.appendLine(vv?.toString() ?: \"null\")\n                        }\n                    }\n                }\n            }\n\n            else -> {\n                sb.append(name)\n                sb.appendLine(v?.toString() ?: \"null\")\n            }\n        }\n    }\n    return sb.toString()\n}\n\noperator fun ViewGroup.iterator(): MutableIterator<View> = object : MutableIterator<View> {\n    private var index = 0\n    override fun hasNext() = index < childCount\n    override fun next() = getChildAt(index++) ?: throw IndexOutOfBoundsException()\n    override fun remove() = removeViewAt(--index)\n}\n\nval ViewGroup.children: Sequence<View>\n    get() = object : Sequence<View> {\n        override fun iterator() = this@children.iterator()\n    }\n\nfun View.setRippleBackground() = with(TypedValue()) {\n    context.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true)\n    setBackgroundResource(resourceId)\n}\n\n@SuppressLint(\"ApplySharedPref\")\nfun migrateHomeFilterPrefsIfNeeded() {\n    if (!sPrefs.getBoolean(\"home_filter_prefs_migrated\", false)) {\n        val titleList = sPrefs.getString(\"keywords_filter_title_recommend_list\", null)\n            ?.split('|')?.filter { it.isNotBlank() }?.toSet()\n        val reasonList = sPrefs.getString(\"keywords_filter_reason_recommend_list\", null)\n            ?.split('|')?.filter { it.isNotBlank() }?.toSet()\n        val uidList = sPrefs.getString(\"keywords_filter_uid_recommend_list\", null)\n            ?.split('|')?.filter { it.isNotBlank() }?.toSet()\n        val upList = sPrefs.getString(\"keywords_filter_upname_recommend_list\", null)\n            ?.split('|')?.filter { it.isNotBlank() }?.toSet()\n        val categoryList = sPrefs.getString(\"keywords_filter_rname_recommend_list\", null)\n            ?.split('|')?.filter { it.isNotBlank() }?.toSet()\n        val channelList = sPrefs.getString(\"keywords_filter_tname_recommend_list\", null)\n            ?.split('|')?.filter { it.isNotBlank() }?.toSet()\n\n        sPrefs.edit().apply {\n            putStringSet(\"home_filter_keywords_title\", titleList)\n            putStringSet(\"home_filter_keywords_reason\", reasonList)\n            putStringSet(\"home_filter_keywords_uid\", uidList)\n            putStringSet(\"home_filter_keywords_up\", upList)\n            putStringSet(\"home_filter_keywords_category\", categoryList)\n            putStringSet(\"home_filter_keywords_channel\", channelList)\n            putBoolean(\"home_filter_prefs_migrated\", true)\n        }.commit()\n    }\n}\n\nfun getRetrofitUrl(response: Any): String? {\n    val requestField = instance.requestField() ?: return null\n    val urlField = instance.urlField() ?: return null\n    val request = response.getObjectField(requestField)\n    return request?.getObjectField(urlField)?.toString()\n}\n\nfun Window.blurBackground() {\n    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return\n    addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)\n    attributes.blurBehindRadius = 50\n    setBackgroundBlurRadius(50)\n    val blurEnableListener = Consumer<Boolean> { enable: Boolean ->\n        setDimAmount(if (enable) 0.1F else 0.6F)\n    }\n    decorView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {\n        @RequiresApi(Build.VERSION_CODES.S)\n        override fun onViewAttachedToWindow(v: View) {\n            windowManager.addCrossWindowBlurEnabledListener(blurEnableListener)\n        }\n\n        @RequiresApi(Build.VERSION_CODES.S)\n        override fun onViewDetachedFromWindow(v: View) {\n            windowManager.removeCrossWindowBlurEnabledListener(blurEnableListener)\n        }\n    })\n    addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)\n}\n\nval Int.sp: Int\n    inline get() = TypedValue.applyDimension(\n        TypedValue.COMPLEX_UNIT_SP,\n        toFloat(),\n        currentContext.resources.displayMetrics\n    ).roundToInt()\n\nval Int.dp: Int\n    inline get() = TypedValue.applyDimension(\n        TypedValue.COMPLEX_UNIT_DIP,\n        toFloat(),\n        currentContext.resources.displayMetrics\n    ).roundToInt()\n\n@Suppress(\"DEPRECATION\")\nval currentIsLandscape: Boolean\n    get() = currentContext.getSystemService(WindowManager::class.java)\n        .defaultDisplay.orientation.let { it == Surface.ROTATION_90 || it == Surface.ROTATION_270 }\n\ninline fun Any.mossResponseHandlerProxy(crossinline onNext: (reply: Any?) -> Unit): Any {\n    return Proxy.newProxyInstance(\n        javaClass.classLoader,\n        arrayOf(instance.mossResponseHandlerClass)\n    ) { _, m, args ->\n        if (m.name == \"onNext\") {\n            val reply = args[0]\n            onNext(reply)\n            m(this, *args)\n        } else if (args == null) {\n            m(this)\n        } else {\n            m(this, *args)\n        }\n    }\n}\n\ninline fun Any.mossResponseHandlerReplaceProxy(crossinline onNext: (reply: Any?) -> Any?): Any {\n    val originalHandler = this\n    return Proxy.newProxyInstance(\n        javaClass.classLoader,\n        arrayOf(instance.mossResponseHandlerClass)\n    ) { _, m, args ->\n        if (m.name == \"onNext\") {\n            onNext(args[0])?.let {\n                args[0] = it\n            }\n            m(this, *args)\n        } else if (m.name == \"onError\") {\n            val newResponse = onNext(null)\n            if (newResponse == null) {\n                m(this, *args)\n            } else {\n                originalHandler.callMethod(\"onNext\", newResponse)\n                originalHandler.callMethod(\"onCompleted\")\n            }\n        } else if (args == null) {\n            m(this)\n        } else {\n            m(this, *args)\n        }\n    }\n}\n\n@SuppressLint(\"ApplySharedPref\")\nfun SharedPreferences.appendStringForSet(key: String, value: String, commit: Boolean = false) {\n    getStringSet(key, null).orEmpty().let {\n        edit().putStringSet(key, it + value).apply { if (commit) commit() else apply() }\n    }\n}\n\nfun ListView.forceSetSelection(pos: Int, top: Int = 0) {\n    // stop scrolling\n    smoothScrollBy(0, 0)\n    setSelectionFromTop(pos, top)\n}\n\nfun Context.inflateLayout(\n    @LayoutRes resource: Int,\n    root: ViewGroup? = null,\n    attachToRoot: Boolean = root != null\n): View = LayoutInflater.from(this).inflate(resource, root, attachToRoot)\n\nfun Context.addModuleAssets() {\n    resources.assets.callMethod(\"addAssetPath\", XposedInit.modulePath)\n}\n\n@OptIn(ExperimentalStdlibApi::class)\nfun Any.dumpToString(): String {\n    val sb = StringBuilder()\n    sb.append(\"---- ${this.javaClass.name}@${this.hashCode().toHexString()} dump begin ----\\n\")\n    fun dumpClassFields(clazz: Class<*>) {\n        clazz.declaredFields.forEach { field ->\n            if (field.isStatic) return@forEach\n            field.isAccessible = true\n            val v = field.get(this)\n            sb.append(\"${field.name}: $v\\n\")\n        }\n        clazz.superclass?.let {\n            dumpClassFields(it)\n        }\n    }\n    dumpClassFields(this.javaClass)\n    sb.append(\"---- ${this.javaClass.name}@${this.hashCode().toHexString()} dump end ----\")\n    return sb.toString()\n}\n\nfun Class<*>.findField(filter: (Field) -> Boolean): Field? {\n    return declaredFields.find { field ->\n        field.isAccessible = true\n        filter(field)\n    } ?: superclass?.findField(filter)\n}\n"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/json/FastjsonHelper.kt",
    "content": "package me.iacn.biliroaming.utils.json\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.callMethod\nimport me.iacn.biliroaming.utils.findField\nimport java.lang.reflect.Field\n\nclass FastjsonHelper : JsonHelper {\n    private lateinit var _data: Any\n    override var data: Any\n        get() = _data\n        set(value) {\n            _data = value\n        }\n\n    override fun getField(key: String): Field {\n        val field = data.javaClass.findField{ field ->\n            val annotation = field.getAnnotation(instance.fastjsonFieldAnnotation as Class<Annotation>) ?: return@findField false\n            annotation.callMethod(\"name\") == key\n        } ?: throw NoSuchFieldException(\"No field found for key: $key in ${data.javaClass.name} or its superclasses\")\n        return field\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/json/GsonHelper.kt",
    "content": "package me.iacn.biliroaming.utils.json\n\nimport me.iacn.biliroaming.BiliBiliPackage.Companion.instance\nimport me.iacn.biliroaming.utils.Log\nimport me.iacn.biliroaming.utils.callMethod\nimport me.iacn.biliroaming.utils.findField\nimport java.lang.reflect.Field\n\nclass GsonHelper : JsonHelper {\n    lateinit var _data: Any\n\n    override var data: Any\n        get() = _data\n        set(value) {\n            _data = value\n        }\n\n    override fun getField(key: String): Field {\n        val field = data.javaClass.findField { field ->\n            val annotation = field.annotations.find {\n                it.toString().startsWith(\"@com.google.gson.annotations.SerializedName(\")\n            }\n            annotation?.callMethod(\"value\") == key\n        } ?: throw NoSuchFieldException(\"No field found for key: $key in ${data.javaClass.name} or its superclasses\")\n        return field\n    }\n}"
  },
  {
    "path": "app/src/main/java/me/iacn/biliroaming/utils/json/JsonHelper.kt",
    "content": "package me.iacn.biliroaming.utils.json\n\nimport java.lang.reflect.Field\n\ninterface JsonHelper {\n    var data: Any\n    fun getField(key: String): Field\n    fun getObject(key: String): Any? {\n        return getField(key).get(data)\n    }\n\n    fun getObjectAsHelper(key: String): JsonHelper {\n        return getObject(key)!!.toJsonHelper(this.javaClass)\n    }\n\n    fun setObject(key: String, value: Any) {\n        getField(key).set(data, value)\n    }\n}\n\nfun <T> JsonHelper.getObjectAs(key: String): T {\n    return getObject(key) as T\n}\n\nfun JsonHelper.getObjectOrNull(key: String): Any? {\n    return try {\n        getObject(key)\n    } catch (e: NoSuchFieldException) {\n        null\n    }\n}\n\nfun JsonHelper.getObjectAsHelperOrNull(key: String): JsonHelper? {\n    return try {\n        getObjectAsHelper(key)\n    } catch (e: NoSuchFieldException) {\n        null\n    }\n}\n\ninline fun <reified T : JsonHelper> Any.toJsonHelper(): T {\n    val jsonHelper = T::class.java.newInstance() as T\n    jsonHelper.data = this\n    return jsonHelper\n}\n\nfun Any.toJsonHelper(jsonHelper: Class<JsonHelper>): JsonHelper {\n    return jsonHelper.newInstance().apply {\n        data = this@toJsonHelper\n    }\n}\n"
  },
  {
    "path": "app/src/main/jni/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.28)\nproject(biliroaming)\n\nset(CMAKE_CXX_SCAN_FOR_MODULES ON)\n\nfind_package(cxx REQUIRED CONFIG)\nlink_libraries(cxx::cxx)\n\nadd_subdirectory(dex_builder)\n\nadd_library(${PROJECT_NAME} SHARED\n        biliroaming.cc\n        )\n\ntarget_link_libraries(${PROJECT_NAME} PUBLIC log dex_builder_static)\n\nif (NOT DEFINED DEBUG_SYMBOLS_PATH)\n    set(DEBUG_SYMBOLS_PATH ${CMAKE_BINARY_DIR}/symbols)\nendif()\n\nadd_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}\n        COMMAND ${CMAKE_STRIP} --strip-all $<TARGET_FILE:${PROJECT_NAME}>)\n"
  },
  {
    "path": "app/src/main/jni/biliroaming.cc",
    "content": "#include <algorithm>\n#include <android/log.h>\n#include <errno.h>\n#include <fcntl.h>\n#include <jni.h>\n#include <list>\n#include <map>\n#include <sys/mman.h>\n#include <sys/stat.h>\n#include <unistd.h>\n#include <zlib.h>\n#include <string>\n#include <string_view>\n\nimport dex_helper;\n\n#define LOG_TAG \"BiliRoaming\"\n#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)\n#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)\n#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)\n#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)\n#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)\n#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)\n#define PLOGE(fmt, args...)                                                    \\\n  LOGE(fmt \" failed with %d: %s\", ##args, errno, strerror(errno))\n\nnamespace {\njfieldID token_field;\njfieldID class_loader_field;\njmethodID load_class_method;\n// jmethodID get_declared_method;\n// jmethodID get_declared_field;\njmethodID get_name_method;\njmethodID get_declaring_class_method;\njmethodID get_parameters_method;\njmethodID get_class_name_method;\njmethodID get_declared_method;\njmethodID get_declared_constructor;\njmethodID get_declared_field;\njfieldID path_list_field;\njfieldID element_field;\njfieldID dex_file_field;\njfieldID cookie_field;\njfieldID file_name_field;\n\nstruct MemMap {\n  MemMap() = default;\n  explicit MemMap(std::string file_name) {\n    int fd = open(file_name.data(), O_RDONLY | O_CLOEXEC);\n    if (fd > 0) {\n      struct stat s {};\n      fstat(fd, &s);\n      auto *addr = mmap(nullptr, s.st_size, PROT_READ, MAP_PRIVATE, fd, 0);\n      if (addr != MAP_FAILED) {\n        addr_ = static_cast<uint8_t *>(addr);\n        len_ = s.st_size;\n      }\n    }\n    close(fd);\n  }\n  explicit MemMap(size_t size) {\n    auto *addr = mmap(nullptr, size, PROT_READ | PROT_WRITE,\n                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n    if (addr != MAP_FAILED) {\n      addr_ = static_cast<uint8_t *>(addr);\n      len_ = size;\n    }\n  }\n  ~MemMap() {\n    if (ok()) {\n      munmap(addr_, len_);\n    }\n  }\n\n  [[nodiscard]] bool ok() const { return addr_ && len_; }\n\n  [[nodiscard]] auto addr() const { return addr_; }\n  [[nodiscard]] auto len() const { return len_; }\n\n  MemMap(MemMap &&other) noexcept : addr_(other.addr_), len_(other.len_) {\n    other.addr_ = nullptr;\n    other.len_ = 0;\n  }\n  MemMap &operator=(MemMap &&other) noexcept {\n    new (this) MemMap(std::move(other));\n    return *this;\n  }\n\n  MemMap(const MemMap &) = delete;\n  MemMap &operator=(const MemMap &) = delete;\n\nprivate:\n  uint8_t *addr_ = nullptr;\n  size_t len_ = 0;\n};\n\nstruct [[gnu::packed]] ZipLocalFile {\n  static ZipLocalFile *from(uint8_t *begin) {\n    auto *file = reinterpret_cast<ZipLocalFile *>(begin);\n    if (file->signature == 0x4034b50u) {\n      return file;\n    } else {\n      return nullptr;\n    }\n  }\n  ZipLocalFile *next() {\n    return from(reinterpret_cast<uint8_t *>(this) + sizeof(ZipLocalFile) +\n                file_name_length + extra_length + compress_size);\n  }\n\n  MemMap uncompress() {\n    if (compress == 0x8) {\n      MemMap out(uncompress_size);\n      if (!out.ok()) {\n        PLOGE(\"failed to mmap for unocmpression\");\n        return {};\n      }\n      z_stream d_stream{\n          .next_in = data(),\n          .avail_in = compress_size,\n          .next_out = out.addr(), /* discard the output */\n          .avail_out = static_cast<uInt>(out.len()),\n          .zalloc = Z_NULL,\n          .zfree = Z_NULL,\n          .opaque = Z_NULL,\n          .data_type = Z_UNKNOWN,\n      }; /* decompression stream */\n\n      for (int err = inflateInit2(&d_stream, -MAX_WBITS); err != Z_STREAM_END;\n           err = inflate(&d_stream, Z_NO_FLUSH)) {\n        if (err != Z_OK) {\n          LOGE(\"inflate %d\", err);\n          return {};\n        }\n      }\n\n      if (int err = inflateEnd(&d_stream); err != Z_OK) {\n        LOGE(\"inflateEnd %d\", err);\n        return {};\n      }\n\n      if (d_stream.total_out != uncompress_size) {\n        LOGE(\"bad inflate: %ld vs %zu\\n\", d_stream.total_out,\n             static_cast<size_t>(uncompress_size));\n        return {};\n      }\n      mprotect(out.addr(), out.len(), PROT_READ);\n      return out;\n    } else if (compress == 0 && compress_size == uncompress_size) {\n      MemMap out(uncompress_size);\n      memcpy(out.addr(), data(), uncompress_size);\n      mprotect(out.addr(), out.len(), PROT_READ);\n      return out;\n    }\n    LOGW(\"unsupported compress type\");\n    return {};\n  }\n\n  std::string_view file_name() { return {name, file_name_length}; }\n\n  uint8_t *data() {\n    return reinterpret_cast<uint8_t *>(this) + sizeof(ZipLocalFile) +\n           file_name_length + extra_length;\n  }\n\n  [[maybe_unused]] uint32_t signature;\n  [[maybe_unused]] uint16_t version;\n  [[maybe_unused]] uint16_t flags;\n  [[maybe_unused]] uint16_t compress;\n  [[maybe_unused]] uint16_t last_modify_time;\n  [[maybe_unused]] uint16_t last_modify_date;\n  [[maybe_unused]] uint32_t crc;\n  [[maybe_unused]] uint32_t compress_size;\n  [[maybe_unused]] uint32_t uncompress_size;\n  [[maybe_unused]] uint16_t file_name_length;\n  [[maybe_unused]] uint16_t extra_length;\n  [[maybe_unused]] char name[0];\n};\n\nclass ZipFile {\npublic:\n  static std::unique_ptr<ZipFile> Open(const MemMap &map) {\n    auto *local_file = ZipLocalFile::from(map.addr());\n    if (!local_file)\n      return nullptr;\n    auto r = std::make_unique<ZipFile>();\n    while (local_file) {\n      r->entries.emplace(local_file->file_name(), local_file);\n      local_file = local_file->next();\n    }\n    return r;\n  }\n  ZipLocalFile *Find(std::string_view entry_name) {\n    if (auto i = entries.find(entry_name); i != entries.end()) {\n      return i->second;\n    }\n    return nullptr;\n  }\n\nprivate:\n  std::map<std::string_view, ZipLocalFile *> entries;\n};\nstatic_assert(offsetof(ZipLocalFile, signature) == 0);\nstatic_assert(offsetof(ZipLocalFile, version) == 4);\nstatic_assert(offsetof(ZipLocalFile, flags) == 6);\nstatic_assert(offsetof(ZipLocalFile, compress) == 8);\nstatic_assert(offsetof(ZipLocalFile, last_modify_time) == 10);\nstatic_assert(offsetof(ZipLocalFile, last_modify_date) == 12);\nstatic_assert(offsetof(ZipLocalFile, crc) == 14);\nstatic_assert(offsetof(ZipLocalFile, compress_size) == 18);\nstatic_assert(offsetof(ZipLocalFile, uncompress_size) == 22);\nstatic_assert(offsetof(ZipLocalFile, file_name_length) == 26);\nstatic_assert(offsetof(ZipLocalFile, name) == 30);\n\nusing Handler = std::tuple<std::unique_ptr<DexHelper>, std::list<MemMap>>;\n\njclass LoadClass(JNIEnv *env, jobject class_loader,\n                 std::string_view descriptor) {\n  if (descriptor.empty())\n    return nullptr;\n  switch (descriptor[0]) {\n    case 'B': {\n        auto box_type = env->FindClass(\"java/lang/Byte\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'C': {\n        auto box_type = env->FindClass(\"java/lang/Character\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'D': {\n        auto box_type = env->FindClass(\"java/lang/Double\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'F': {\n        auto box_type = env->FindClass(\"java/lang/Float\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'I': {\n        auto box_type = env->FindClass(\"java/lang/Integer\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'J': {\n        auto box_type = env->FindClass(\"java/lang/Long\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'S': {\n        auto box_type = env->FindClass(\"java/lang/Short\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'Z': {\n        auto box_type = env->FindClass(\"java/lang/Boolean\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case 'V': {\n        auto box_type = env->FindClass(\"java/lang/Void\");\n        static auto field = env->GetStaticFieldID(box_type, \"TYPE\", \"Ljava/lang/Class;\");\n        auto res = (jclass) env->GetStaticObjectField(box_type, field);\n        env->DeleteLocalRef(box_type);\n        return res;\n    }\n    case '[': {\n        std::string name(descriptor);\n        std::replace(name.begin(), name.end(), '/', '.');\n        auto java_name = env->NewStringUTF(name.data());\n        auto res = (jclass)env->CallObjectMethod(class_loader, load_class_method,\n                                                 java_name, false);\n        return res;\n    }\n    case 'L':\n      break;\n    default:\n      return nullptr;\n  }\n  std::string name(descriptor.substr(1, descriptor.length() - 2));\n  std::replace(name.begin(), name.end(), '/', '.');\n  auto java_name = env->NewStringUTF(name.data());\n  auto res = (jclass)env->CallObjectMethod(class_loader, load_class_method,\n                                           java_name, false);\n  return res;\n}\n\njobject LoadField(JNIEnv *env, jclass clazz, std::string_view name) {\n    auto java_name = env->NewStringUTF(name.data());\n    auto res = env->CallObjectMethod(clazz, get_declared_field, java_name);\n    env->DeleteLocalRef(java_name);\n    if (env->ExceptionCheck()) {\n        env->DeleteLocalRef(env->ExceptionOccurred());\n        env->ExceptionClear();\n        return nullptr;\n    }\n    return res;\n}\n\njobject LoadMethod(JNIEnv *env, jobject class_loader, jclass clazz, std::string_view name, std::vector<std::string_view>& params) {\n  auto java_name = env->NewStringUTF(name.data());\n  auto lang_class = env->FindClass(\"java/lang/Class\");\n  auto params_class = env->NewObjectArray(static_cast<int>(params.size()), lang_class, nullptr);\n  for (int i = 0; auto descriptor: params) {\n    auto param = LoadClass(env, class_loader, descriptor);\n    env->SetObjectArrayElement(params_class, i++, param);\n    env->DeleteLocalRef(param);\n  }\n  auto res = name == \"<init>\"\n             ? env->CallObjectMethod(clazz, get_declared_constructor, params_class)\n             : env->CallObjectMethod(clazz, get_declared_method, java_name, params_class);\n  env->DeleteLocalRef(java_name);\n  env->DeleteLocalRef(params_class);\n  if (env->ExceptionCheck()) {\n    env->DeleteLocalRef(env->ExceptionOccurred());\n    env->ExceptionClear();\n    return nullptr;\n  }\n  return res;\n}\n\nstd::string GetClassDescriptor(JNIEnv *env, jclass clazz) {\n  auto java_name = (jstring)env->CallObjectMethod(clazz, get_class_name_method);\n  auto name = env->GetStringUTFChars(java_name, nullptr);\n  std::string descriptor = name;\n  std::replace(descriptor.begin(), descriptor.end(), '.', '/');\n  if (descriptor[0] != '[') {\n    if (descriptor == \"boolean\") {\n      descriptor = \"Z\";\n    } else if (descriptor == \"byte\") {\n      descriptor = \"B\";\n    } else if (descriptor == \"short\") {\n      descriptor = \"S\";\n    } else if (descriptor == \"char\") {\n      descriptor = \"C\";\n    } else if (descriptor == \"int\") {\n      descriptor = \"I\";\n    } else if (descriptor == \"long\") {\n      descriptor = \"J\";\n    } else if (descriptor == \"float\") {\n      descriptor = \"F\";\n    } else if (descriptor == \"double\") {\n      descriptor = \"D\";\n    } else if (descriptor == \"void\") {\n      descriptor = \"V\";\n    } else {\n      descriptor = \"L\" + descriptor + \";\";\n    }\n  }\n  env->ReleaseStringUTFChars(java_name, name);\n  return descriptor;\n}\n} // namespace\n\nstruct MyDexFile {\n  const void *begin_{};\n  size_t size_{};\n\n  virtual ~MyDexFile() = default;\n};\n\nextern \"C\" JNIEXPORT jlongArray JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_findMethodUsingString(\n    JNIEnv *env, jobject thiz, jstring str, jboolean match_prefix,\n    jlong return_type, jshort parameter_count, jstring parameter_shorty,\n    jlong declaring_class, jlongArray parameter_types,\n    jlongArray contains_parameter_types, jintArray dex_priority,\n    jboolean find_first) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return env->NewLongArray(0);\n  auto &[helper, _] = *handler;\n  if (!str)\n    return env->NewLongArray(0);\n  auto str_ = env->GetStringUTFChars(str, nullptr);\n  auto parameter_shorty_ =\n      parameter_shorty ? env->GetStringUTFChars(parameter_shorty, nullptr)\n                       : nullptr;\n  std::vector<size_t> dex_priority_;\n  jint *dex_priority_elements = nullptr;\n  if (dex_priority) {\n    dex_priority_elements = env->GetIntArrayElements(dex_priority, nullptr);\n    dex_priority_.assign(dex_priority_elements,\n                         dex_priority_elements +\n                             env->GetArrayLength(dex_priority));\n  }\n\n  std::vector<size_t> parameter_types_;\n  jlong *parameter_types_elements = nullptr;\n  if (parameter_types) {\n    parameter_types_elements =\n        env->GetLongArrayElements(parameter_types, nullptr);\n    parameter_types_.assign(parameter_types_elements,\n                            parameter_types_elements +\n                                env->GetArrayLength(parameter_types));\n  }\n\n  std::vector<size_t> contains_parameter_types_;\n  jlong *contains_parameter_types_elements = nullptr;\n  if (contains_parameter_types) {\n    contains_parameter_types_elements =\n        env->GetLongArrayElements(contains_parameter_types, nullptr);\n    contains_parameter_types_.assign(\n        contains_parameter_types_elements,\n        contains_parameter_types_elements +\n            env->GetArrayLength(contains_parameter_types));\n  }\n\n  auto out = helper->FindMethodUsingString(\n      str_, match_prefix, return_type, parameter_count,\n      parameter_shorty_ ? parameter_shorty_ : \"\", declaring_class,\n      parameter_types_, contains_parameter_types_, dex_priority_, find_first);\n\n  env->ReleaseStringUTFChars(str, str_);\n  if (parameter_shorty_)\n    env->ReleaseStringUTFChars(parameter_shorty, parameter_shorty_);\n  if (dex_priority_elements)\n    env->ReleaseIntArrayElements(dex_priority, dex_priority_elements,\n                                 JNI_ABORT);\n  if (parameter_types_elements)\n    env->ReleaseLongArrayElements(parameter_types, parameter_types_elements,\n                                  JNI_ABORT);\n  if (contains_parameter_types_elements)\n    env->ReleaseLongArrayElements(contains_parameter_types,\n                                  contains_parameter_types_elements, JNI_ABORT);\n  auto res = env->NewLongArray(static_cast<int>(out.size()));\n  auto res_element = env->GetLongArrayElements(res, nullptr);\n  for (size_t i = 0; i < out.size(); ++i) {\n    res_element[i] = static_cast<jlong>(out[i]);\n  }\n  env->ReleaseLongArrayElements(res, res_element, 0);\n  return res;\n}\n\nextern \"C\" JNIEXPORT jlong JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_load(JNIEnv *env, jobject thiz,\n                                              jobject class_loader) {\n  if (!class_loader)\n    return 0;\n  auto path_list = env->GetObjectField(class_loader, path_list_field);\n  if (!path_list)\n    return 0;\n  auto elements = (jobjectArray)env->GetObjectField(path_list, element_field);\n  if (!elements)\n    return 0;\n  std::vector<std::tuple<const void *, size_t, const void *, size_t>> images;\n  std::list<MemMap> maps;\n  for (auto i = 0, len = env->GetArrayLength(elements); i < len; ++i) {\n    auto element = env->GetObjectArrayElement(elements, i);\n    if (!element)\n      continue;\n    auto java_dex_file = env->GetObjectField(element, dex_file_field);\n    if (!java_dex_file)\n      continue;\n    auto cookie = (jlongArray)env->GetObjectField(java_dex_file, cookie_field);\n    if (!cookie)\n      continue;\n    auto dex_file_length = env->GetArrayLength(cookie);\n    const auto *dex_files = reinterpret_cast<const MyDexFile **>(\n        env->GetLongArrayElements(cookie, nullptr));\n    std::vector<std::tuple<const void *, size_t, const void *, size_t>>\n        dex_images;\n    if (!dex_files[0]) {\n      while (dex_file_length-- > 1) {\n        const auto *dex_file = dex_files[dex_file_length];\n        LOGD(\"Got dex file %d\", dex_file_length);\n        if (!dex_file) {\n          LOGD(\"Skip empty dex file\");\n          dex_images.clear();\n          break;\n        }\n        if (dex::Reader::IsCompact(dex_file->begin_)) {\n          LOGD(\"skip compact dex\");\n          dex_images.clear();\n          break;\n        } else {\n          dex_images.emplace_back(dex_file->begin_, dex_file->size_, nullptr,\n                                  0);\n        }\n      }\n    }\n    if (dex_images.empty()) {\n      // contains compact dex, try to load from original file\n      auto file_name_obj =\n          (jstring)env->GetObjectField(java_dex_file, file_name_field);\n      if (!file_name_obj)\n        continue;\n      auto file_name = env->GetStringUTFChars(file_name_obj, nullptr);\n      LOGD(\"dex filename is %s\", file_name);\n      auto map = MemMap(file_name);\n      if (!map.ok())\n        continue;\n      auto zip_file = ZipFile::Open(map);\n      if (zip_file) {\n        for (int idx = 1;; ++idx) {\n          auto entry = zip_file->Find(\n              \"classes\" + (idx == 1 ? std::string() : std::to_string(idx)) +\n              \".dex\");\n          if (entry) {\n            auto uncompress = entry->uncompress();\n            if (uncompress.ok()) {\n              LOGD(\"uncompressed %.*s\",\n                   static_cast<int>(entry->file_name().size()),\n                   entry->file_name().data());\n              images.emplace_back(uncompress.addr(), uncompress.len(), nullptr,\n                                  0);\n              maps.emplace_back(std::move(uncompress));\n            } else {\n              LOGW(\"failed to uncompressed %.*s\",\n                   static_cast<int>(entry->file_name().size()),\n                   entry->file_name().data());\n            }\n          } else {\n            break;\n          }\n        }\n      } else {\n        images.emplace_back(map.addr(), map.len(), nullptr, 0);\n        maps.emplace_back(std::move(map));\n      }\n      env->ReleaseStringUTFChars(file_name_obj, file_name);\n      env->DeleteLocalRef(file_name_obj);\n    } else {\n      for (auto &image : dex_images) {\n        images.emplace_back(std::move(image));\n      }\n    }\n  }\n  if (images.empty())\n    return 0;\n  auto res = reinterpret_cast<jlong>(\n      new Handler(std::make_unique<DexHelper>(images), std::move(maps)));\n  env->SetLongField(thiz, token_field, res);\n  return res;\n}\n\nextern \"C\" JNIEXPORT jlongArray JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_findMethodInvoking(\n    JNIEnv *env, jobject thiz, jlong method_index, jlong return_type,\n    jshort parameter_count, jstring parameter_shorty, jlong declaring_class,\n    jlongArray parameter_types, jlongArray contains_parameter_types,\n    jintArray dex_priority, jboolean find_first) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return env->NewLongArray(0);\n  auto &[helper, _] = *handler;\n  auto parameter_shorty_ =\n      parameter_shorty ? env->GetStringUTFChars(parameter_shorty, nullptr)\n                       : nullptr;\n  std::vector<size_t> dex_priority_;\n  jint *dex_priority_elements = nullptr;\n  if (dex_priority) {\n    dex_priority_elements = env->GetIntArrayElements(dex_priority, nullptr);\n    dex_priority_.assign(dex_priority_elements,\n                         dex_priority_elements +\n                             env->GetArrayLength(dex_priority));\n  }\n\n  std::vector<size_t> parameter_types_;\n  jlong *parameter_types_elements = nullptr;\n  if (parameter_types) {\n    parameter_types_elements =\n        env->GetLongArrayElements(parameter_types, nullptr);\n    parameter_types_.assign(parameter_types_elements,\n                            parameter_types_elements +\n                                env->GetArrayLength(parameter_types));\n  }\n\n  std::vector<size_t> contains_parameter_types_;\n  jlong *contains_parameter_types_elements = nullptr;\n  if (contains_parameter_types) {\n    contains_parameter_types_elements =\n        env->GetLongArrayElements(contains_parameter_types, nullptr);\n    contains_parameter_types_.assign(\n        contains_parameter_types_elements,\n        contains_parameter_types_elements +\n            env->GetArrayLength(contains_parameter_types));\n  }\n\n  auto out = helper->FindMethodInvoking(\n      method_index, return_type, parameter_count,\n      parameter_shorty_ ? parameter_shorty_ : \"\", declaring_class,\n      parameter_types_, contains_parameter_types_, dex_priority_, find_first);\n\n  if (parameter_shorty_)\n    env->ReleaseStringUTFChars(parameter_shorty, parameter_shorty_);\n  if (dex_priority_elements)\n    env->ReleaseIntArrayElements(dex_priority, dex_priority_elements,\n                                 JNI_ABORT);\n  if (parameter_types_elements)\n    env->ReleaseLongArrayElements(parameter_types, parameter_types_elements,\n                                  JNI_ABORT);\n  if (contains_parameter_types_elements)\n    env->ReleaseLongArrayElements(contains_parameter_types,\n                                  contains_parameter_types_elements, JNI_ABORT);\n  auto res = env->NewLongArray(static_cast<int>(out.size()));\n  auto res_element = env->GetLongArrayElements(res, nullptr);\n  for (size_t i = 0; i < out.size(); ++i) {\n    res_element[i] = static_cast<jlong>(out[i]);\n  }\n  env->ReleaseLongArrayElements(res, res_element, 0);\n  return res;\n}\nextern \"C\" JNIEXPORT jlongArray JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_findMethodInvoked(\n    JNIEnv *env, jobject thiz, jlong method_index, jlong return_type,\n    jshort parameter_count, jstring parameter_shorty, jlong declaring_class,\n    jlongArray parameter_types, jlongArray contains_parameter_types,\n    jintArray dex_priority, jboolean find_first) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return env->NewLongArray(0);\n  auto &[helper, _] = *handler;\n  auto parameter_shorty_ =\n      parameter_shorty ? env->GetStringUTFChars(parameter_shorty, nullptr)\n                       : nullptr;\n  std::vector<size_t> dex_priority_;\n  jint *dex_priority_elements = nullptr;\n  if (dex_priority) {\n    dex_priority_elements = env->GetIntArrayElements(dex_priority, nullptr);\n    dex_priority_.assign(dex_priority_elements,\n                         dex_priority_elements +\n                             env->GetArrayLength(dex_priority));\n  }\n\n  std::vector<size_t> parameter_types_;\n  jlong *parameter_types_elements = nullptr;\n  if (parameter_types) {\n    parameter_types_elements =\n        env->GetLongArrayElements(parameter_types, nullptr);\n    parameter_types_.assign(parameter_types_elements,\n                            parameter_types_elements +\n                                env->GetArrayLength(parameter_types));\n  }\n\n  std::vector<size_t> contains_parameter_types_;\n  jlong *contains_parameter_types_elements = nullptr;\n  if (contains_parameter_types) {\n    contains_parameter_types_elements =\n        env->GetLongArrayElements(contains_parameter_types, nullptr);\n    contains_parameter_types_.assign(\n        contains_parameter_types_elements,\n        contains_parameter_types_elements +\n            env->GetArrayLength(contains_parameter_types));\n  }\n\n  auto out = helper->FindMethodInvoked(\n      method_index, return_type, parameter_count,\n      parameter_shorty_ ? parameter_shorty_ : \"\", declaring_class,\n      parameter_types_, contains_parameter_types_, dex_priority_, find_first);\n\n  if (parameter_shorty_)\n    env->ReleaseStringUTFChars(parameter_shorty, parameter_shorty_);\n  if (dex_priority_elements)\n    env->ReleaseIntArrayElements(dex_priority, dex_priority_elements,\n                                 JNI_ABORT);\n  if (parameter_types_elements)\n    env->ReleaseLongArrayElements(parameter_types, parameter_types_elements,\n                                  JNI_ABORT);\n  if (contains_parameter_types_elements)\n    env->ReleaseLongArrayElements(contains_parameter_types,\n                                  contains_parameter_types_elements, JNI_ABORT);\n  auto res = env->NewLongArray(static_cast<int>(out.size()));\n  auto res_element = env->GetLongArrayElements(res, nullptr);\n  for (size_t i = 0; i < out.size(); ++i) {\n    res_element[i] = static_cast<jlong>(out[i]);\n  }\n  env->ReleaseLongArrayElements(res, res_element, 0);\n  return res;\n}\nextern \"C\" JNIEXPORT jlongArray JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_findMethodSettingField(\n    JNIEnv *env, jobject thiz, jlong field_index, jlong return_type,\n    jshort parameter_count, jstring parameter_shorty, jlong declaring_class,\n    jlongArray parameter_types, jlongArray contains_parameter_types,\n    jintArray dex_priority, jboolean find_first) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return env->NewLongArray(0);\n  auto &[helper, _] = *handler;\n  auto parameter_shorty_ =\n      parameter_shorty ? env->GetStringUTFChars(parameter_shorty, nullptr)\n                       : nullptr;\n  std::vector<size_t> dex_priority_;\n  jint *dex_priority_elements = nullptr;\n  if (dex_priority) {\n    dex_priority_elements = env->GetIntArrayElements(dex_priority, nullptr);\n    dex_priority_.assign(dex_priority_elements,\n                         dex_priority_elements +\n                             env->GetArrayLength(dex_priority));\n  }\n\n  std::vector<size_t> parameter_types_;\n  jlong *parameter_types_elements = nullptr;\n  if (parameter_types) {\n    parameter_types_elements =\n        env->GetLongArrayElements(parameter_types, nullptr);\n    parameter_types_.assign(parameter_types_elements,\n                            parameter_types_elements +\n                                env->GetArrayLength(parameter_types));\n  }\n\n  std::vector<size_t> contains_parameter_types_;\n  jlong *contains_parameter_types_elements = nullptr;\n  if (contains_parameter_types) {\n    contains_parameter_types_elements =\n        env->GetLongArrayElements(contains_parameter_types, nullptr);\n    contains_parameter_types_.assign(\n        contains_parameter_types_elements,\n        contains_parameter_types_elements +\n            env->GetArrayLength(contains_parameter_types));\n  }\n\n  auto out = helper->FindMethodSettingField(\n      field_index, return_type, parameter_count,\n      parameter_shorty_ ? parameter_shorty_ : \"\", declaring_class,\n      parameter_types_, contains_parameter_types_, dex_priority_, find_first);\n\n  if (parameter_shorty_)\n    env->ReleaseStringUTFChars(parameter_shorty, parameter_shorty_);\n  if (dex_priority_elements)\n    env->ReleaseIntArrayElements(dex_priority, dex_priority_elements,\n                                 JNI_ABORT);\n  if (parameter_types_elements)\n    env->ReleaseLongArrayElements(parameter_types, parameter_types_elements,\n                                  JNI_ABORT);\n  if (contains_parameter_types_elements)\n    env->ReleaseLongArrayElements(contains_parameter_types,\n                                  contains_parameter_types_elements, JNI_ABORT);\n  auto res = env->NewLongArray(static_cast<int>(out.size()));\n  auto res_element = env->GetLongArrayElements(res, nullptr);\n  for (size_t i = 0; i < out.size(); ++i) {\n    res_element[i] = static_cast<jlong>(out[i]);\n  }\n  env->ReleaseLongArrayElements(res, res_element, 0);\n  return res;\n}\nextern \"C\" JNIEXPORT jlongArray JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_findMethodGettingField(\n    JNIEnv *env, jobject thiz, jlong field_index, jlong return_type,\n    jshort parameter_count, jstring parameter_shorty, jlong declaring_class,\n    jlongArray parameter_types, jlongArray contains_parameter_types,\n    jintArray dex_priority, jboolean find_first) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return env->NewLongArray(0);\n  auto &[helper, _] = *handler;\n  auto parameter_shorty_ =\n      parameter_shorty ? env->GetStringUTFChars(parameter_shorty, nullptr)\n                       : nullptr;\n  std::vector<size_t> dex_priority_;\n  jint *dex_priority_elements = nullptr;\n  if (dex_priority) {\n    dex_priority_elements = env->GetIntArrayElements(dex_priority, nullptr);\n    dex_priority_.assign(dex_priority_elements,\n                         dex_priority_elements +\n                             env->GetArrayLength(dex_priority));\n  }\n\n  std::vector<size_t> parameter_types_;\n  jlong *parameter_types_elements = nullptr;\n  if (parameter_types) {\n    parameter_types_elements =\n        env->GetLongArrayElements(parameter_types, nullptr);\n    parameter_types_.assign(parameter_types_elements,\n                            parameter_types_elements +\n                                env->GetArrayLength(parameter_types));\n  }\n\n  std::vector<size_t> contains_parameter_types_;\n  jlong *contains_parameter_types_elements = nullptr;\n  if (contains_parameter_types) {\n    contains_parameter_types_elements =\n        env->GetLongArrayElements(contains_parameter_types, nullptr);\n    contains_parameter_types_.assign(\n        contains_parameter_types_elements,\n        contains_parameter_types_elements +\n            env->GetArrayLength(contains_parameter_types));\n  }\n\n  auto out = helper->FindMethodGettingField(\n      field_index, return_type, parameter_count,\n      parameter_shorty_ ? parameter_shorty_ : \"\", declaring_class,\n      parameter_types_, contains_parameter_types_, dex_priority_, find_first);\n\n  if (parameter_shorty_)\n    env->ReleaseStringUTFChars(parameter_shorty, parameter_shorty_);\n  if (dex_priority_elements)\n    env->ReleaseIntArrayElements(dex_priority, dex_priority_elements,\n                                 JNI_ABORT);\n  if (parameter_types_elements)\n    env->ReleaseLongArrayElements(parameter_types, parameter_types_elements,\n                                  JNI_ABORT);\n  if (contains_parameter_types_elements)\n    env->ReleaseLongArrayElements(contains_parameter_types,\n                                  contains_parameter_types_elements, JNI_ABORT);\n  auto res = env->NewLongArray(static_cast<int>(out.size()));\n  auto res_element = env->GetLongArrayElements(res, nullptr);\n  for (size_t i = 0; i < out.size(); ++i) {\n    res_element[i] = static_cast<jlong>(out[i]);\n  }\n  env->ReleaseLongArrayElements(res, res_element, 0);\n  return res;\n}\nextern \"C\" JNIEXPORT jlongArray JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_findField(JNIEnv *env, jobject thiz,\n                                                   jlong type,\n                                                   jintArray dex_priority,\n                                                   jboolean find_first) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return env->NewLongArray(0);\n  auto &[helper, _] = *handler;\n  std::vector<size_t> dex_priority_;\n  jint *dex_priority_elements = nullptr;\n  if (dex_priority) {\n    dex_priority_elements = env->GetIntArrayElements(dex_priority, nullptr);\n    dex_priority_.assign(dex_priority_elements,\n                         dex_priority_elements +\n                             env->GetArrayLength(dex_priority));\n  }\n\n  auto out = helper->FindField(type, dex_priority_, find_first);\n\n  if (dex_priority_elements)\n    env->ReleaseIntArrayElements(dex_priority, dex_priority_elements,\n                                 JNI_ABORT);\n\n  auto res = env->NewLongArray(static_cast<int>(out.size()));\n  auto res_element = env->GetLongArrayElements(res, nullptr);\n  for (size_t i = 0; i < out.size(); ++i) {\n    res_element[i] = static_cast<jlong>(out[i]);\n  }\n  env->ReleaseLongArrayElements(res, res_element, 0);\n  return res;\n}\n\nextern \"C\" JNIEXPORT jobject JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_decodeMethodIndex(JNIEnv *env,\n                                                           jobject thiz,\n                                                           jlong method_index) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return nullptr;\n  auto &[helper, _] = *handler;\n  auto out = helper->DecodeMethod(method_index);\n  auto cl = env->GetObjectField(thiz, class_loader_field);\n  auto clazz = LoadClass(env, cl, out.declaring_class.name);\n  if (!clazz)\n    return nullptr;\n  std::vector<std::string_view> params(out.parameters.size());\n  for (size_t i = 0; i < out.parameters.size(); ++i) {\n    params[i] = out.parameters[i].name;\n  }\n  auto res = LoadMethod(env, cl, clazz, out.name, params);\n  env->DeleteLocalRef(cl);\n  env->DeleteLocalRef(clazz);\n  return res;\n}\n\nextern \"C\" JNIEXPORT jobject JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_decodeFieldIndex(JNIEnv *env,\n                                                          jobject thiz,\n                                                          jlong field_index) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return nullptr;\n  auto &[helper, _] = *handler;\n  auto out = helper->DecodeField(field_index);\n  auto cl = env->GetObjectField(thiz, class_loader_field);\n  auto clazz = LoadClass(env, cl, out.declaring_class.name);\n  if (!clazz)\n    return nullptr;\n  env->DeleteLocalRef(cl);\n  auto res = LoadField(env, clazz, out.name);\n  env->DeleteLocalRef(clazz);\n  return res;\n}\n\nextern \"C\" JNIEXPORT jlong JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_encodeClassIndex(JNIEnv *env,\n                                                          jobject thiz,\n                                                          jclass clazz) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return -1;\n  auto &[helper, _] = *handler;\n  return static_cast<jlong>(\n      helper->CreateClassIndex(GetClassDescriptor(env, clazz)));\n}\n\nextern \"C\" JNIEXPORT jlong JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_encodeFieldIndex(JNIEnv *env,\n                                                          jobject thiz,\n                                                          jobject field) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return -1;\n  auto &[helper, _] = *handler;\n  auto java_name = (jstring)env->CallObjectMethod(field, get_name_method);\n  auto clazz = (jclass)env->CallObjectMethod(field, get_declaring_class_method);\n  auto name = env->GetStringUTFChars(java_name, nullptr);\n  auto res = helper->CreateFieldIndex(GetClassDescriptor(env, clazz), name);\n  env->ReleaseStringUTFChars(java_name, name);\n  return static_cast<jlong>(res);\n}\n\nextern \"C\" JNIEXPORT jlong JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_encodeMethodIndex(JNIEnv *env,\n                                                           jobject thiz,\n                                                           jobject method) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return -1;\n  auto &[helper, _] = *handler;\n  auto java_name = (jstring)env->CallObjectMethod(method, get_name_method);\n  auto name = env->GetStringUTFChars(java_name, nullptr);\n  auto clazz =\n      (jclass)env->CallObjectMethod(method, get_declaring_class_method);\n  auto constructor_clazz = env->FindClass(\"java/lang/reflect/Constructor\");\n  auto real_name = env->IsInstanceOf(method, constructor_clazz) ? \"<init>\" : name;\n  auto params =\n      (jobjectArray)env->CallObjectMethod(method, get_parameters_method);\n  auto param_len = env->GetArrayLength(params);\n  std::vector<std::string> param_descriptors;\n  param_descriptors.reserve(param_len);\n  for (int i = 0; i < param_len; ++i) {\n    auto param = (jclass)env->GetObjectArrayElement(params, i);\n    param_descriptors.emplace_back(GetClassDescriptor(env, param));\n    env->DeleteLocalRef(param);\n  }\n  std::vector<std::string_view> params_name;\n  params_name.reserve(param_descriptors.size());\n  for (const auto &descriptor : param_descriptors) {\n    params_name.emplace_back(descriptor);\n  }\n  auto res = helper->CreateMethodIndex(GetClassDescriptor(env, clazz), real_name,\n                                       params_name);\n  env->ReleaseStringUTFChars(java_name, name);\n  return static_cast<jlong>(res);\n}\n\nextern \"C\" JNIEXPORT jclass JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_decodeClassIndex(JNIEnv *env,\n                                                          jobject thiz,\n                                                          jlong class_index) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return nullptr;\n  auto &[helper, _] = *handler;\n  auto out = helper->DecodeClass(class_index);\n  auto cl = env->GetObjectField(thiz, class_loader_field);\n  auto res = LoadClass(env, cl, out.name);\n  env->DeleteLocalRef(cl);\n  return res;\n}\n\nextern \"C\" JNIEXPORT void JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_close(JNIEnv *env, jobject thiz) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  env->SetLongField(thiz, token_field, jlong(0));\n  delete handler;\n}\n\nextern \"C\" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {\n  JNIEnv *env = nullptr;\n  if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK) {\n    return -1;\n  }\n  jclass helper = env->FindClass(\"me/iacn/biliroaming/utils/DexHelper\");\n  token_field = env->GetFieldID(helper, \"token\", \"J\");\n  class_loader_field =\n      env->GetFieldID(helper, \"classLoader\", \"Ljava/lang/ClassLoader;\");\n  auto class_loader = env->FindClass(\"java/lang/ClassLoader\");\n  load_class_method = env->GetMethodID(\n      class_loader, \"loadClass\", \"(Ljava/lang/String;Z)Ljava/lang/Class;\");\n  auto member = env->FindClass(\"java/lang/reflect/Member\");\n  get_declaring_class_method =\n      env->GetMethodID(member, \"getDeclaringClass\", \"()Ljava/lang/Class;\");\n  get_name_method = env->GetMethodID(member, \"getName\", \"()Ljava/lang/String;\");\n  jclass executable = env->FindClass(\"java/lang/reflect/Executable\");\n  if (!executable) {\n    env->ExceptionClear();\n    executable = env->FindClass(\"java/lang/reflect/AbstractMethod\");\n  }\n  get_parameters_method =\n      env->GetMethodID(executable, \"getParameterTypes\", \"()[Ljava/lang/Class;\");\n  auto clazz = env->FindClass(\"java/lang/Class\");\n  get_class_name_method =\n      env->GetMethodID(clazz, \"getName\", \"()Ljava/lang/String;\");\n  get_declared_method = env->GetMethodID(clazz, \"getDeclaredMethod\",\n      \"(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;\");\n  get_declared_constructor = env->GetMethodID(clazz, \"getDeclaredConstructor\",\n      \"([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;\");\n  get_declared_field = env->GetMethodID(clazz, \"getDeclaredField\",\n      \"(Ljava/lang/String;)Ljava/lang/reflect/Field;\");\n  auto dex_class_loader = env->FindClass(\"dalvik/system/BaseDexClassLoader\");\n  path_list_field = env->GetFieldID(dex_class_loader, \"pathList\",\n                                    \"Ldalvik/system/DexPathList;\");\n  auto dex_path_list = env->FindClass(\"dalvik/system/DexPathList\");\n  element_field = env->GetFieldID(dex_path_list, \"dexElements\",\n                                  \"[Ldalvik/system/DexPathList$Element;\");\n  auto element = env->FindClass(\"dalvik/system/DexPathList$Element\");\n  dex_file_field =\n      env->GetFieldID(element, \"dexFile\", \"Ldalvik/system/DexFile;\");\n  auto dex_file = env->FindClass(\"dalvik/system/DexFile\");\n  cookie_field = env->GetFieldID(dex_file, \"mCookie\", \"Ljava/lang/Object;\");\n  file_name_field =\n      env->GetFieldID(dex_file, \"mFileName\", \"Ljava/lang/String;\");\n  return JNI_VERSION_1_4;\n}\n\nextern \"C\" JNIEXPORT void JNICALL\nJava_me_iacn_biliroaming_utils_DexHelper_createFullCache(JNIEnv *env,\n                                                         jobject thiz) {\n  auto *handler =\n      reinterpret_cast<Handler *>(env->GetLongField(thiz, token_field));\n  if (!handler)\n    return;\n  auto &[helper, _] = *handler;\n  helper->CreateFullCache();\n}\n"
  },
  {
    "path": "app/src/main/proto/me/iacn/biliroaming/api.proto",
    "content": "syntax = \"proto3\";\n\nimport \"google/protobuf/any.proto\";\n\npackage me.iacn.biliroaming;\n\noption java_package = \"me.iacn.biliroaming\";\noption java_outer_classname = \"API\";\noption java_multiple_files = true;\noption optimize_for = LITE_RUNTIME;\n\nenum CodeType {\n  NOCODE = 0;\n  CODE264 = 1;\n  CODE265 = 2;\n  CODEAV1 = 3;\n}\n\nmessage SceneControl {\n}\n\nmessage DataControl {\n  optional bool need_watch_progress = 0x1;\n}\n\nmessage PlayViewReq {\n  optional int64 ep_id = 0x1;\n  optional int64 cid = 0x2;\n  optional int64 qn = 0x3;\n  optional int32 fnver = 0x4;\n  optional int32 fnval = 0x5;\n  optional int32 download = 0x6;\n  optional int32 force_host = 0x7;\n  optional bool fourk = 0x8;\n  optional string spmid = 0x9;\n  optional string from_spmid = 0xa;\n  optional int32 teenagers_mode = 0xb;\n  optional CodeType prefer_codec_type = 0xc;\n  optional bool is_preview = 0xd;\n  optional int32 room_id = 0xe;\n  optional bool is_need_view_info = 0xf;\n  optional SceneControl scene_control = 0x10;\n  optional int32 inline_scene = 0x11;\n  optional int64 material_no = 0x12;\n  optional int32 security_level = 0x13;\n  optional int64 season_id = 0x14;\n  optional DataControl data_control = 0x15;\n\n}\n\nmessage ContinuePlayInfo {\n\n}\n\nmessage ClipInfo {\n\n}\n\nmessage QualityExtInfo {\n\n}\n\nmessage RecordInfo {\n\n}\n\nmessage UserStatus {\n}\n\nmessage SeasonRights {\n  optional int32 can_watch = 0x1;\n}\n\nmessage SeasonInfo {\n  optional int32 season_id = 0x1;\n  optional int32 season_type = 0x2;\n  optional int32 season_status = 0x3;\n  optional string cover = 0x4;\n  optional string title = 0x5;\n  optional SeasonRights rights = 0x6;\n}\n\nmessage EpisodeInfo {\n  optional int32 ep_id = 0x1;\n  optional int64 cid = 0x2;\n  optional int64 aid = 0x3;\n  optional int64 ep_status = 0x4;\n  optional SeasonInfo season_info = 0x5;\n  optional string cover = 0x6;\n  optional string title = 0x7;\n  optional Interaction interaction = 0x8;\n}\n\nmessage EpisodeAdvertisementInfo {\n\n}\n\nmessage BusinessInfo {\n  optional bool is_preview = 0x1;\n  optional bool bp = 0x2;\n  optional string marlin_token = 0x3;\n  optional string playback_speed_color = 0x4;\n  optional ContinuePlayInfo continue_play_info = 0x5;\n  repeated ClipInfo clip_info = 0x6;\n  optional int32 inline_type = 0x7;\n  optional int32 ep_whole_duration = 0x8;\n  optional PlayViewDimension dimension = 0x9;\n  repeated QualityExtInfo quality_ext_map = 0xa;\n  map<string, int32> ext_map = 0xb;\n  optional bool drm_tech_type = 0xc;\n  optional int32 limit_action_type = 0xd;\n  optional bool is_drm = 0xe;\n  optional RecordInfo record_info = 0xf;\n  optional int32 vip_status = 0x10;\n  optional bool is_live_pre = 0x11;\n  optional EpisodeInfo episode_info = 0x12;\n  optional EpisodeAdvertisementInfo episode_advertisement_info = 0x13;\n  optional UserStatus user_status = 0x14;\n}\n\nmessage PlayAbilityConf {\n  optional bool background_play_disable = 0x1;\n  optional bool flip_disable = 0x2;\n  optional bool cast_disable = 0x3;\n  optional bool feedback_disable = 0x4;\n  optional bool subtitle_disable = 0x5;\n  optional bool playback_rate_disable = 0x6;\n  optional bool time_up_disable = 0x7;\n  optional bool playback_mode_disable = 0x8;\n  optional bool scale_mode_disable = 0x9;\n  optional bool like_disable = 0xa;\n  optional bool dislike_disable = 0xb;\n  optional bool coin_disable = 0xc;\n  optional bool elec_disable = 0xd;\n  optional bool share_disable = 0xe;\n  optional bool screen_shot_disable = 0xf;\n  optional bool lock_screen_disable = 0x10;\n  optional bool recommend_disable = 0x11;\n  optional bool playback_speed_disable = 0x12;\n  optional bool definition_disable = 0x13;\n  optional bool selections_disable = 0x14;\n  optional bool next_disable = 0x15;\n  optional bool edit_dm_disable = 0x16;\n  optional bool freya_enter_disable = 0x1b;\n  optional bool freya_full_disable = 0x1d;\n}\n\nmessage StreamLimit {\n  optional string title = 0x1;\n  optional string uri = 0x2;\n  optional string msg = 0x3;\n}\n\nmessage StreamInfo {\n  optional int32 quality = 0x1;\n  optional string format = 0x2;\n  optional string description = 0x3;\n  optional int32 err_code = 0x4;\n  optional StreamLimit limit = 0x5;\n  optional bool need_vip = 0x6;\n  optional bool need_login = 0x7;\n  optional bool intact = 0x8;\n  optional bool no_rexcode = 0x9;\n  optional int64 attribute = 0xa;\n  optional string new_description = 0xb;\n  optional string display_desc = 0xc;\n  optional string superscript = 0xd;\n}\n\nmessage DashVideo {\n  optional string base_url = 0x1;\n  repeated string backup_url = 0x2;\n  optional int32 bandwidth = 0x3;\n  optional int32 codecid = 0x4;\n  optional string md5 = 0x5;\n  optional int64 size = 0x6;\n  optional int32 audio_id = 0x7;\n  optional bool no_rexcode = 0x8;\n}\n\nmessage ResponseUrl {\n  repeated string backup_url = 0x5;\n  optional int64 length = 0x2;\n  optional string md5 = 0x6;\n  optional int32 order = 0x1;\n  optional int64 size = 0x3;\n  optional string url = 0x4;\n}\n\nmessage SegmentVideo {repeated ResponseUrl segment = 0x1;}\n\nmessage Stream {\n  optional StreamInfo stream_info = 0x1;\n  oneof content {\n    DashVideo dash_video = 0x2;\n    SegmentVideo segment_video = 0x3;\n  }\n}\n\nmessage DashItem {\n  optional int32 id = 0x1;\n  optional string base_url = 0x2;\n  repeated string backup_url = 0x3;\n  optional int32 bandwidth = 0x4;\n  optional int32 codecid = 0x5;\n  optional string md5 = 0x6;\n  optional int64 size = 0x7;\n}\n\nmessage VideoInfo {\n  optional int32 quality = 0x1;\n  optional string format = 0x2;\n  optional int64 timelength = 0x3;\n  optional int32 video_codecid = 0x4;\n  repeated Stream stream_list = 0x5;\n  repeated DashItem dash_audio = 0x6;\n}\n\nmessage Event {}\n\nmessage TextInfo {\n  optional string text = 0x1;\n  optional string text_color = 0x2;\n  optional string text_color_night = 0x3;\n}\n\nmessage DialogConfig {}\n\nmessage ImageInfo {optional string url = 0x1;}\n\nmessage ButtonInfo {\n  optional string action_type = 0x7;\n}\n\nmessage Report {}\n\nmessage BottomDisplay {}\n\nmessage Dialog {\n  optional int64 code = 0x1;\n  optional string msg = 0x2;\n  optional string type = 0x3;\n  optional string style = 0x4;\n  optional DialogConfig config = 0x5;\n  optional TextInfo title = 0x6;\n  optional TextInfo subtitle = 0x7;\n  optional ImageInfo image = 0x8;\n  repeated ButtonInfo button = 0x9;\n  optional BusinessInfo button_desc = 0xa;\n  optional Report report = 0xb;\n  optional int32 count_down_sec = 0xc;\n  optional TextInfo right_bottom_desc = 0xd;\n  repeated BottomDisplay bottom_display = 0xe;\n}\n\nmessage Toast {\n  optional string text = 0x1;\n  optional ButtonInfo button = 0x2;\n}\n\nmessage CouponInfo {\n  optional int64 code = 0x1;\n  optional string msg = 0x2;\n  optional string type = 0x3;\n}\n\nmessage EndPage {\n  optional Dialog dialog = 0x1;\n  optional bool hide = 0x2;\n}\n\nmessage PopWin {\n  repeated ButtonInfo button = 0x3;\n}\n\nmessage PromptBar {\n  repeated ButtonInfo button = 0x6;\n}\n\nmessage PayTip {}\n\nmessage HighDefinitionTrialInfo {}\n\nmessage Animation {}\n\nmessage ViewInfo {\n  optional Dialog dialog = 0x1;\n  optional Toast toast = 0x2;\n  optional CouponInfo coupon_info = 0x3;\n  repeated int64 demand_no_pay_epids = 0x4;\n  optional EndPage end_page = 0x5;\n  map<string, bool> exp_config = 0x6;\n  optional PopWin pop_win = 0x7;\n  optional PromptBar try_watch_prompt_bar = 0x8;\n  optional PayTip pay_tip = 0x9;\n  optional HighDefinitionTrialInfo high_definition_trial_info = 0xa;\n  map<string, Dialog> ext_dialog = 0xb;\n  optional Animation animation = 0xc;\n  map<string, Toast> ext_toast = 0xd;\n}\n\nmessage FreyaConfig {}\n\nmessage PlayAbilityExtConf {\n  optional bool allow_close_subtitle = 0x1;\n  optional FreyaConfig freya_config = 0x2;\n}\n\nmessage PlayViewReply {\n  optional VideoInfo video_info = 0x1;\n  optional PlayAbilityConf play_conf = 0x2;\n  optional BusinessInfo business = 0x3;\n  optional Event event = 0x4;\n  optional ViewInfo view_info = 0x5;\n  optional PlayAbilityExtConf play_ext_conf = 0x6;\n}\n\nmessage ExtraContent {}\n\nmessage ArcConf {\n  optional bool is_support = 0x1;\n  optional bool disabled = 0x2;\n  optional ExtraContent extra_content = 0x3;\n}\n\nmessage PlayArcConf {\n  map<int32, ArcConf> arc_conf = 0x1;\n}\n\nmessage PlayDeviceConf {}\nmessage PlayArc {\n  optional BizType video_type = 0x1;\n  optional uint64 aid = 0x2;\n  optional uint64 cid = 0x3;\n  optional DrmTechType drm_tech_type = 0x4;\n  optional ArcType arc_type = 0x5;\n  optional Interaction interaction = 0x6;\n  optional Dimension dimension = 0x7;\n  optional int64 duration = 0x8;\n  optional bool is_preview = 0x9;\n}\nenum BizType {\n  BIZ_TYPE_UNKNOWN = 0x0;\n  BIZ_TYPE_UGC = 0x1;\n  BIZ_TYPE_PGC = 0x2;\n  BIZ_TYPE_PUGV = 0x3;\n}\nenum DrmTechType {\n  UNKNOWN_DRM = 0x0;\n  FAIR_PLAY = 0x1;\n  WIDE_VINE = 0x2;\n  BILI_DRM = 0x3;\n}\nenum ArcType {\n  ARC_TYPE_NORMAL = 0x0;\n  ARC_TYPE_INTERACT = 0x1;\n}\nmessage QnTrialInfo {}\n\n// renamed due to name collision\nmessage PlaysharedViewInfo {\n  map<string, PlaysharedDialog> dialog_map = 0x1;\n}\nmessage PlaysharedDialog {\n    int32 style_type = 0x1;\n    BackgroundInfo background_info = 0x2;\n    TextInfo title = 0x3;\n    TextInfo subtitle = 0x4;\n    ImageInfo image = 0x5;\n    repeated ButtonInfo button = 0x6;\n    ButtonInfo bottom_desc = 0x7;\n    int32 count_down_sec = 0x9;\n    TextInfo right_bottom_desc= 0xa;\n    repeated BottomDisplay bottom_display= 0xb;\n    ExtData ext_data= 0xc;\n    int32 limit_action_type= 0xd;\n}\nmessage BackgroundInfo {\n  optional string drawable_color = 0x1;\n  optional string drawable_bitmap_url = 0x2;\n  optional int32 effects = 0x3;\n}\nmessage ExtData {}\nmessage PlayViewUniteReply {\n  optional VideoInfo vod_info = 0x1;\n  optional PlayArcConf play_arc_conf = 0x2;\n  optional PlayDeviceConf play_device_conf = 0x3;\n  optional Event event = 0x4;\n  optional google.protobuf.Any supplement = 0x5;\n  optional PlayArc play_arc = 0x6;\n  optional QnTrialInfo qn_trial_info = 0x7;\n  optional History history = 0x8;\n  optional PlaysharedViewInfo view_info = 0x9;\n}\n\nmessage VideoVod {\n  optional int64 aid = 0x1;\n  optional int64 cid = 0x2;\n  optional int64 qn = 0x3;\n  optional int32 fnver = 0x4;\n  optional int32 fnval = 0x5;\n  optional int32 download = 0x6;\n  optional int32 force_host = 0x7;\n  optional bool fourk = 0x8;\n  optional CodeType prefer_code_type = 0x9;\n  optional int64 voice_balance = 0xa;\n}\n\nmessage PlayViewUniteReq {\n  optional VideoVod vod = 0x1;\n  optional string spmid = 0x2;\n  optional string from_spmid = 0x3;\n  map<string, string> extra_content = 0x4;\n}\n\nmessage ViewReq {\n  optional int64 aid = 0x1;\n  optional string bvid = 0x2;\n  optional string from = 0x3;\n  optional string trackid = 0x4;\n  optional string ad_extra = 0x5;\n  optional int32 qn = 0x6;\n  optional int32 fnver = 0x7;\n  optional int32 fnval = 0x8;\n  optional int32 force_host = 0x9;\n  optional int32 fourk = 0xa;\n  optional string spmid = 0xb;\n  optional string from_spmid = 0xc;\n  optional int32 autoplay = 0xd;\n  optional PlayerArgs player_args = 0xe;\n}\n\nmessage ViewUniteReq {\n  optional uint64 aid = 0x1;\n  optional string bvid = 0x2;\n  optional string from = 0x3;\n  optional string spmid = 0x4;\n  optional string from_spmid = 0x5;\n  optional string session_id = 0x6;\n  optional PlayerArgs player_args = 0x7;\n  optional string track_id = 0x8;\n  map<string, string> extra_content = 0x9;\n  optional string play_mode = 0xa;\n  optional Relate relate = 0xb;\n  optional string biz_extra = 0xc;\n  optional string ad_extra = 0xd;\n}\n\nmessage UserSeason {optional string attention = 0x1;}\n\nmessage SeasonPlayer {\n  optional int64 aid = 0x1;\n  optional string vid = 0x2;\n  optional int64 cid = 0x3;\n  optional string from = 0x4;\n}\n\nmessage Rights {\n  optional int32 bp = 0x1;\n  optional int32 elec = 0x2;\n  optional int32 download = 0x3;\n  optional int32 movie = 0x4;\n  optional int32 pay = 0x5;\n  optional int32 hd5 = 0x6;\n  optional int32 no_reprint = 0x7;\n  optional int32 autoplay = 0x8;\n  optional int32 ugc_pay = 0x9;\n  optional int32 is_cooperation = 0xa;\n  optional int32 ugc_pay_preview = 0xb;\n  optional int32 no_background = 0xc;\n}\n\nmessage Stat {\n  optional int64 aid = 0x1;\n  optional int32 view = 0x2;\n  optional int32 danmaku = 0x3;\n  optional int32 reply = 0x4;\n  optional int32 fav = 0x5;\n  optional int32 coin = 0x6;\n  optional int32 share = 0x7;\n  optional int32 now_rank = 0x8;\n  optional int32 his_rank = 0x9;\n  optional int32 like = 0xa;\n  optional int32 dislike = 0xb;\n}\n\nmessage Author {\n  optional int64 mid = 0x1;\n  optional string name = 0x2;\n  optional string face = 0x3;\n}\n\nmessage Dimension {\n  optional int64 width = 0x1;\n  optional int64 height = 0x2;\n  optional int64 rotate = 0x3;\n}\n\nmessage PlayViewDimension {\n  optional int32 width = 0x1;\n  optional int32 height = 0x2;\n  optional int32 rotate = 0x3;\n}\n\nmessage StaffInfo {}\n\nmessage Arc {\n  optional int64 aid = 0x1;\n  optional int64 videos = 0x2;\n  optional int32 type_id = 0x3;\n  optional string type_name = 0x4;\n  optional int32 copyright = 0x5;\n  optional string pic = 0x6;\n  optional string title = 0x7;\n  optional int64 pubdate = 0x8;\n  optional int64 ctime = 0x9;\n  optional string desc = 0xa;\n  optional int32 state = 0xb;\n  optional int32 access = 0xc;\n  optional int64 attribute_v2 = 0xd;\n  optional string tag = 0xe;\n  repeated string tags = 0xf;\n  optional int64 duration = 0x10;\n  optional int64 mission_id = 0x11;\n  optional int64 order_id = 0x12;\n  optional string redirect_url = 0x13;\n  optional int64 forward = 0x14;\n  optional Rights rights = 0x15;\n  optional Author author = 0x16;\n  optional Stat stat = 0x17;\n  optional string report_result = 0x18;\n  optional string dynamic = 0x19;\n  optional int64 first_cid = 0x1a;\n  optional Dimension dimension = 0x1b;\n  repeated StaffInfo staff_info = 0x1c;\n  optional int64 season_id = 0x1d;\n  optional int32 attribute = 0x1e;\n}\n\nmessage Season {\n  optional string allow_download = 0x1;\n  optional int64 season_id = 0x2;\n  optional int32 is_jump = 0x3;\n  optional string title = 0x4;\n  optional string cover = 0x5;\n  optional int32 is_finish = 0x6;\n  optional int32 newest_epid = 0x7;\n  optional string newest_ep_index = 0x8;\n  optional int64 total_count = 0x9;\n  optional int32 weekday = 0xa;\n  optional UserSeason user_season = 0xb;\n  optional SeasonPlayer player = 0xc;\n  optional string ovg_playurl = 0xd;\n}\n\nmessage Bgm {}\n\nmessage CMConfig {}\n\nmessage CM {}\n\nmessage ListenerConfig {\n  optional int32 jump_style = 0x1;\n}\n\nmessage Config {\n  optional string relates_title = 0x1;\n  optional int32 relates_style = 0x2;\n  optional int32 relate_gif_exp = 0x3;\n  optional int32 end_page_half = 0x4;\n  optional int32 end_page_full = 0x5;\n  optional bool auto_swindow = 0x6;\n  optional bool popup_info = 0x7;\n  optional string abtest_small_window = 0x8;\n  optional int32 rec_three_point_style = 0x9;\n  optional bool is_absolute_time = 0xa;\n  optional bool new_swindow = 0xb;\n  optional bool relates_biserial = 0xc;\n  optional ListenerConfig listener_conf = 0xd;\n  optional string relates_feed_style = 0xe;\n  optional bool relates_feed_popup = 0xf;\n  optional bool relates_feed_has_next = 0x10;\n  optional int32 local_play = 0x11;\n  optional bool play_story = 0x12;\n  optional bool arc_play_story = 0x13;\n  optional string story_icon = 0x14;\n  optional bool landscape_story = 0x15;\n  optional bool arc_landscape_story = 0x16;\n  optional string landscape_icon = 0x17;\n  optional bool show_listen_button = 0x18 ;\n}\n\nmessage CustomConfig {}\n\nmessage Dislike {}\n\nmessage ElecRank {}\n\nmessage History {}\nmessage Honor {}\n\nmessage Interaction {}\n\nmessage Label {}\n\nmessage OfficialVerify {\n  optional int32 type = 0x1;\n  optional string desc = 0x2;\n}\n\nmessage VipLabel {\n  optional string path = 0x1;\n  optional string text = 0x2;\n  optional string label_theme = 0x3;\n}\n\nmessage Vip {\n  optional int32 vip_type = 0x1;\n  optional int64 due_date = 0x2;\n  optional string due_remark = 0x3;\n  optional int32 access_status = 0x4;\n  optional int32 vip_status = 0x5;\n  optional string vip_status_warn = 0x6;\n  optional int32 theme_type = 0x7;\n  optional VipLabel label = 0x8;\n}\n\nmessage Live {\n  optional int64 mid = 0x1;\n  optional int64 room_id = 0x2;\n  optional string uri = 0x3;\n  optional string endpage_uri = 0x4;\n}\n\nmessage OwnerExt {\n  optional OfficialVerify official_verify = 0x1;\n  optional Live live = 0x2;\n  optional Vip vip = 0x3;\n  repeated int64 assists = 0x4;\n  optional int64 fans = 0x5;\n  optional string arcCount = 0x6;\n}\n\nmessage Audio {}\nmessage DM {}\nmessage Page {\n  optional int64 cid = 0x1;\n  optional int32 page = 0x2;\n  optional string from = 0x3;\n  optional string part = 0x4;\n  optional int64 duration = 0x5;\n  optional string vid = 0x6;\n  optional string desc = 0x7;\n  optional string web_link = 0x8;\n  optional Dimension dimension = 0x9;\n}\n\nmessage ViewPage {\n  optional Page page = 0x1;\n  optional Audio audio = 0x2;\n  optional DM dm = 0x3;\n  optional string download_title = 0x4;\n  optional string download_subtitle = 0x5;\n}\n\nmessage PlayerIcon {}\n\nmessage RelateTab {}\nmessage Relate {}\nmessage ReqUser {}\nmessage Staff {}\nmessage TIcon {optional string icon = 0x1;}\nmessage Tag {\n  optional int64 id = 0x1;\n  optional string name = 0x2;\n  optional int64 likes = 0x3;\n  optional int64 hates = 0x4;\n  optional int32 liked = 0x5;\n  optional int32 hated = 0x6;\n  optional string uri = 0x7;\n  optional string tag_type = 0x8;\n}\nmessage UgcSeason {}\nmessage ViewReply {\n  optional string activity_url = 0x11;\n  optional Arc arc = 0x1;\n  optional string argue_msg = 0x14;\n  repeated Bgm bgm = 0x12;\n  optional string bvid = 0xe;\n  optional CMConfig cm_config = 0x1f;\n  repeated CM cms = 0x1e;\n  optional Config config = 0x19;\n  optional CustomConfig custom_config = 0x1d;\n  optional Dislike dislike = 0xb;\n  optional int32 encode = 0x1c;\n  optional ElecRank elec_rank = 0x8;\n  optional History history = 0x9;\n  optional Honor honor = 0xf;\n  optional Interaction interaction = 0x1b;\n  optional Label label = 0x17;\n  optional OwnerExt owner_ext = 0x3;\n  repeated ViewPage pages = 0x2;\n  optional int32 play_param = 0x16;\n  optional PlayerIcon player_icon = 0xc;\n  repeated RelateTab relate_tab = 0x10;\n  repeated Relate relates = 0xa;\n  optional ReqUser req_user = 0x4;\n\n  optional Season season = 0x7;\n  optional string share_subtitle = 0x1a;\n  optional string short_link = 0x15;\n  repeated Staff staff = 0x13;\n  map<string, TIcon> t_icon = 0x6;\n  repeated Tag tag = 0x5;\n  optional UgcSeason ugc_season = 0x18;\n  optional string vip_active = 0xd;\n}\nmessage ViewUniteOfficialVerify {\n  optional int32 type = 0x1;\n  optional string desc = 0x2;\n}\nmessage ViewUniteVipLabel {\n  optional string path = 0x1;\n  optional string text = 0x2;\n  optional string label_theme = 0x3;\n}\nmessage ViewUniteVip {\n  optional int32 type = 0x1;\n  optional int32 status = 0x2;\n  optional int32 theme_type = 0x3;\n  optional ViewUniteVipLabel vip_label = 0x4;\n  optional int32 is_vip = 0x5;\n}\nmessage ViewUniteOwner {\n  optional string title = 0x3;\n  optional string fans = 0x4;\n  optional string arc_count = 0x5;\n  optional ViewUniteVip vip = 0x9;\n  optional string face = 0xb;\n  optional int64 mid = 0xc;\n  optional ViewUniteOfficialVerify official_verify = 0xd;\n}\nmessage ViewUniteReply {\n  optional ViewBase view_base = 0x1;\n  optional ViewUniteArc arc = 0x2;\n  optional ViewUniteOwner owner = 0x4;\n  optional Tab tab = 0x5;\n  optional google.protobuf.Any supplement = 0x6;\n}\nmessage ViewBase {\n  enum PageType {\n    H5 = 0x0;\n    NA = 0x1;\n  }\n  optional int32 biz_type = 0x1;\n  optional PageType page_type = 0x2;\n}\nmessage ViewUniteStat {\n  optional int64 reply = 0x3;\n  optional int64 fav = 0x4;\n  optional int64 coin = 0x5;\n  optional int64 share = 0x6;\n  optional int64 like = 0x7;\n  optional int64 follow = 0x8;\n}\n// Renamed due to name collision\nmessage ViewUniteArc {\n  optional int64 aid = 0x1;\n  optional int64 cid = 0x2;\n  optional int64 duration = 0x3;\n  optional ViewUniteStat stat = 0x4;\n  optional string bvid = 0x5;\n  optional int32 copyright = 0x6;\n  optional ViewUniteArcRights right = 0x7;\n  optional string cover = 0x8;\n  optional int64 type_id = 0x9;\n  optional string title= 0xa;\n}\n// Renamed due to name collision\nmessage ViewUniteArcRights {\n  optional bool only_vip_download = 0x1;\n  optional bool no_reprint = 0x2;\n  optional bool download = 0x3;\n}\nmessage UgcDimension {\n  optional int64 width = 0x1;\n  optional int64 height = 0x2;\n  optional int64 rotate = 0x3;\n}\nmessage UgcPage {\n  optional int64 cid = 0x1;\n  optional string part = 0x2;\n  optional int64 duration = 0x3;\n  optional string desc = 0x4;\n  optional UgcDimension dimension = 0x5;\n  optional string dl_title = 0x6;\n  optional string dl_subtitle = 0x7;\n}\nmessage ViewUgcAny {\n  optional string short_link = 0x3;\n  optional string share_subtitle = 0x4;\n  repeated UgcPage pages = 0x5;\n}\nmessage ViewPgcAny {\n  optional OgvData ogv_data = 0x1;\n}\nmessage OgvData {\n  optional int32 media_id = 0x1;\n  optional int64 season_id = 0x2;\n  optional int32 season_type = 0x3;\n  optional int32 show_season_type = 0x4;\n  optional OgvDataRights rights = 0x5;\n  optional OgvDataUserStatus user_status = 0x6;\n  optional int64 aid = 0x7;\n  optional OgvDataStat stat = 0x8;\n  optional int32 mode = 0x9;\n  optional Publish publish = 0xa;\n  optional PlayStrategy play_strategy = 0xb;\n  optional MultiViewInfo multi_view_info = 0xc;\n  optional OgvSwitch ogv_switch = 0xd;\n  optional int32 total_ep = 0xe;\n  optional NewEp new_ep = 0xf;\n  optional Reserve reserve = 0x10;\n  optional int32 status = 0x11;\n  repeated PlayFloatLayerActivity activity_float_layer = 0x12;\n  optional EarphoneConf earphone_conf = 0x13;\n  optional string cover = 0x14;\n  optional string square_cover = 0x15;\n  optional string share_url = 0x16;\n  optional string short_link = 0x17;\n  optional string title = 0x18;\n  optional string horizontal_cover169 = 0x19;\n  optional string horizontal_cover1610 = 0x1a;\n  optional int32 has_can_play_ep = 0x1b;\n}\nmessage OgvDataRights {\n  optional int32 allow_download = 0x1;\n  optional int32 allow_review = 0x2;\n  optional int32 can_watch = 0x3;\n  optional int32 is_cover_show = 0x4;\n  optional string copyright = 0x5;\n  optional string copyright_name = 0x6;\n  optional int32 allow_bp = 0x7;\n  optional int32 area_limit = 0x8;\n  optional int32 is_preview = 0x9;\n  optional int32 ban_area_show = 0xa;\n  optional int32 watch_platform = 0xb;\n  optional int32 allow_bp_rank = 0xc;\n  optional string resource = 0xd;\n  optional int32 forbid_pre = 0xe;\n  optional int32 only_vip_download = 0xf;\n  optional int32 new_allow_download = 0x10;\n}\nmessage OgvDataUserStatus {\n  optional int32 show = 0x1;\n  optional int32 follow = 0x2;\n  optional int32 follow_status = 0x3;\n  optional int32 pay = 0x4;\n  optional int32 sponsor = 0x5;\n  optional int32 vip = 0x6;\n  optional int32 vip_frozen = 0x7;\n}\nmessage OgvDataStat {\n  optional string followers = 0x1;\n  optional StatInfo play_data = 0x2;\n}\nmessage StatInfo {\n  optional int64 value = 0x1;\n  optional string text = 0x2;\n  optional string pure_text = 0x3;\n  optional string icon = 0x4;\n}\nmessage Publish {\n  optional string pub_time = 0x1;\n  optional string pub_time_show = 0x2;\n  optional int32 is_started = 0x3;\n  optional int32 is_finish = 0x4;\n  optional int32 weekday = 0x5;\n  optional string release_date_show = 0x6;\n  optional string time_length_show = 0x7;\n  optional int32 unknow_pub_date = 0x8;\n  optional string update_info_desc = 0x9;\n}\nmessage NewEp {\n  optional int32 id = 0x1;\n  optional string title = 0x2;\n  optional string desc = 0x3;\n  optional int32 is_new = 0x4;\n  optional string more = 0x5;\n  optional string cover = 0x6;\n  optional string index_show = 0x7;\n}\nmessage PlayStrategy {\n  repeated string strategies = 0x1;\n  optional int32 recommend_show_strategy = 0x2;\n  optional string auto_play_toast = 0x3;\n}\nmessage MultiViewInfo {}\nmessage OgvSwitch {\n  optional int32 reduce_short_title_spacing = 0x1;\n  optional int32 merge_position_section_for_cinema = 0x2;\n  optional int32 merge_preview_section = 0x3;\n  optional int32 enable_show_vt_info = 0x4;\n}\nmessage Reserve {}\nmessage PlayFloatLayerActivity {}\nmessage EarphoneConf {}\n\nmessage Tab {\n  repeated TabModule tab_module = 0x1;\n}\nmessage TabModule {\n  optional TabType tab_type = 0x1;\n  oneof tab {\n    IntroductionTab introduction = 0x2;\n    ReplyTab reply = 0x3;\n    ActivityTab activity_tab = 0x4;\n  }\n}\nenum TabType {\n  TAB_NONE = 0x0;\n  TAB_INTRODUCTION = 0x1;\n  TAB_REPLY = 0x2;\n  TAB_OGV_ACTIVITY = 0x3;\n}\nmessage IntroductionTab {\n  string title = 0x1;\n  repeated Module modules = 0x2;\n}\nmessage Module {\n  ModuleType type = 0x1;\n  oneof data {\n      OgvIntroduction ogv_introduction = 0x2;\n      UgcIntroduction ugc_introduction = 0x3;\n      KingPosition king_position = 0x4;\n      Headline head_line = 0x5;\n      OgvTitle ogv_title = 0x6;\n      Honor honor = 0x7;\n      UserList list = 0x8;\n      Staffs staffs = 0x9;\n      ActivityReserve activity_reserve= 0xa;\n      LiveOrder live_order= 0xb;\n      SectionData section_data= 0xc;\n      DeliveryData delivery_data= 0xd;\n      FollowLayer follow_layer= 0xe;\n      OgvSeasons ogv_seasons= 0xf;\n      UgcSeasons ugc_season = 0x10;\n      OgvLiveReserve ogv_live_reserve = 0x11;\n      CombinationEp combination_ep = 0x12;\n      Sponsor sponsor = 0x13;\n      ActivityEntranceModule activity_entrance_module = 0x14;\n      SerialSeason serial_season = 0x15;\n      Relates relates = 0x16;\n      Banner banner = 0x17;\n      Audio audio = 0x18;\n      LikeComment like_comment = 0x19;\n      AttentionRecommend attention_recommend = 0x1a;\n      Covenanter covenanter = 0x1b;\n  }\n}\nenum ModuleType {\n  UNKNOWN = 0x0;\n  OGV_INTRODUCTION = 0x1;\n  OGV_TITLE = 0x2;\n  UGC_HEADLINE = 0x3;\n  UGC_INTRODUCTION = 0x4;\n  KING_POSITION = 0x5;\n  MASTER_USER_LIST = 0x6;\n  STAFFS = 0x7;\n  HONOR = 0x8;\n  OWNER = 0x9;\n  PAGE= 0xa;\n  ACTIVITY_RESERVE= 0xb;\n  LIVE_ORDER= 0xc;\n  POSITIVE= 0xd;\n  SECTION= 0xe;\n  RELATE= 0xf;\n  PUGV = 0x10;\n  COLLECTION_CARD = 0x11;\n  ACTIVITY = 0x12;\n  CHARACTER = 0x13;\n  FOLLOW_LAYER = 0x14;\n  OGV_SEASONS = 0x15;\n  UGC_SEASON = 0x16;\n  OGV_LIVE_RESERVE = 0x17;\n  COMBINATION_EPISODE = 0x18;\n  SPONSOR = 0x19;\n  ACTIVITY_ENTRANCE = 0x1a;\n  THEATRE_HOT_TOPIC = 0x1b;\n  RELATED_RECOMMEND = 0x1c;\n  PAY_BAR = 0x1d;\n  BANNER = 0x1e;\n  AUDIO = 0x1f;\n  AGG_CARD = 0x20;\n  SINGLE_EP = 0x21;\n  LIKE_COMMENT = 0x22;\n  ATTENTION_RECOMMEND = 0x23;\n  COVENANTER = 0x24;\n}\nmessage OgvIntroduction {\n  optional string followers = 0x1;\n  optional string score = 0x2;\n  optional StatInfo play_data = 0x3;\n}\nenum DescType {\n    DescTypeUnknown = 0;\n    DescTypeText = 1;\n    DescTypeAt = 2;\n}\nmessage DescV2 {\n  optional string text = 0x1;\n  optional DescType type = 0x2;\n\n  optional int64 rid = 0x4;\n}\nmessage UgcTag {\n  optional int64 id = 0x1;\n  optional string name = 0x2;\n  optional string uri = 0x3;\n  optional string tag_type = 0x4;\n}\nmessage UgcIntroduction {\n  repeated UgcTag tags = 0x1;\n  optional int64 pubdate = 0x7;\n  repeated DescV2 desc = 0x8;\n}\nmessage KingPosition {\n  repeated KingPos king_pos = 0x1;\n  repeated KingPos extenf = 0x2;\n}\nmessage KingPos {\n  enum KingPositionType {\n    KING_POS_UNSPECIFIED = 0x0;\n    LIKE = 0x1;\n    DISLIKE = 0x2;\n    COIN = 0x3;\n    FAV = 0x4;\n    SHARE = 0x5;\n    CACHE = 0x6;\n    DANMAKU = 0x7;\n  }\n  message LikeExtend {}\n  message CoinExtend {}\n  optional bool disable = 0x1;\n  optional string icon = 0x2;\n  optional KingPositionType type = 0x3;\n  optional string disable_toast = 0x4;\n  optional string checked_post = 0x5;\n  oneof extend {\n    LikeExtend like = 0x6;\n    CoinExtend coin = 0x7;\n  }\n}\nmessage Headline {\n  optional string content = 0x2;\n}\nmessage OgvTitle {\n  optional string title = 0x1;\n  optional BadgeInfo badge_info = 0x2;\n  optional int32 is_show_btn_animation = 0x3;\n  optional int32 follow_video_is_reserve_live = 0x4;\n  optional int64 reserve_id = 0x5;\n}\nmessage BadgeInfo {\n  optional string text = 0x1;\n  optional string text_color = 0x2;\n  optional string text_color_night = 0x3;\n  optional string bg_color = 0x4;\n  optional string bg_color_night = 0x5;\n  optional string border_color = 0x6;\n  optional string border_color_night = 0x7;\n  optional int32 bg_style = 0x8;\n  optional string img = 0x9;\n  optional int32 type= 0xa;\n}\nmessage UserList {}\nmessage Staffs {}\nmessage ActivityReserve {}\nmessage LiveOrder {}\nmessage SectionData {\n  optional int32 id = 0x1;\n  optional int32 section_id = 0x2;\n  optional string title = 0x3;\n  optional int32 can_ord_desc = 0x4;\n  optional string more = 0x5;\n  repeated int32 episode_ids = 0x6;\n  repeated ViewEpisode episodes = 0x7;\n  optional string split_text = 0x8;\n  optional Style module_style = 0x9;\n  optional string more_bottom_desc= 0xa;\n  repeated SerialSeason seasons= 0xb;\n  optional SectionDataButton more_left= 0xc;\n  optional int32 type= 0xd;\n}\nmessage ViewEpisode {\n  optional int64 ep_id = 0x1;\n  optional string badge = 0x2;\n  optional int32 badge_type = 0x3;\n  optional BadgeInfo badge_info = 0x4;\n  optional int32 duration = 0x5;\n  optional int32 status = 0x6;\n  optional string cover = 0x7;\n  optional int64 aid = 0x8;\n  optional string title = 0x9;\n  optional string movie_title= 0xa;\n  optional string subtitle= 0xb;\n  optional string long_title= 0xc;\n  optional string toast_title= 0xd;\n  optional int64 cid= 0xe;\n  optional string from= 0xf;\n  optional string share_url = 0x10;\n  optional string share_copy = 0x11;\n  optional string short_link = 0x12;\n  optional string vid = 0x13;\n  optional string release_date = 0x14;\n  optional Dimension dimension = 0x15;\n  optional ViewEpisodeRights rights = 0x16;\n  optional Interaction interaction = 0x17;\n  optional string bvid = 0x18;\n  optional int32 archive_attr = 0x19;\n  optional string link = 0x1a;\n  optional string link_type = 0x1b;\n  optional string bmid = 0x1c;\n  optional int64 pub_time = 0x1d;\n  optional int32 pv = 0x1e;\n  optional int32 ep_index = 0x1f;\n  optional int32 section_index = 0x20;\n  repeated Staff up_infos = 0x21;\n  optional Staff up_info = 0x22;\n  optional string dialog_type = 0x23;\n  optional string toast_type = 0x24;\n  optional bool is_sub_view = 38;\n  optional bool is_view_hide = 39;\n  optional string jump_link = 40;\n  optional ViewEpisodeStat stat_for_unity = 41;\n}\nmessage ViewEpisodeRights {\n  optional int32 allow_download = 0x1;\n  optional int32 allow_review = 0x2;\n  optional int32 can_watch = 0x3;\n  optional string resource = 0x4;\n  optional int32 allow_dm = 0x5;\n  optional int32 allow_demand = 0x6;\n  optional int32 area_limit = 0x7;\n}\nmessage ViewEpisodeStat {\n  StatInfo vt = 0x1;\n  StatInfo danmaku = 0x2;\n  int64 reply = 0x3;\n  int64 fav = 0x4;\n  int64 coin = 0x5;\n  int64 share = 0x6;\n  int64 like = 0x7;\n  int64 follow = 0x8;\n}\nmessage Style {\n  optional int32 line = 0x1;\n  optional int32 hidden = 0x2;\n  repeated string show_pages = 0x3;\n}\nmessage SerialSeason {\n  optional int32 season_id = 0x1;\n  optional string title = 0x2;\n  optional string season_title = 0x3;\n  optional int32 is_new = 0x4;\n  optional string cover = 0x5;\n  optional string badge = 0x6;\n  optional int32 badge_type = 0x7;\n  optional BadgeInfo badge_info = 0x8;\n  optional string link = 0x9;\n  optional string resource= 0xa;\n  optional NewEp new_ep= 0xb;\n}\nmessage SectionDataButton {\n  optional string title = 0x1;\n  optional string left_strikethrough_text = 0x2;\n  optional string type = 0x3;\n  optional string link = 0x4;\n  optional BadgeInfo badge_info = 0x5;\n  optional string sub_title = 0x6;\n}\nmessage DeliveryData {}\nmessage FollowLayer {}\nmessage OgvSeasons {\n  enum SerialSeasonCoverStyle {\n    TITLE = 0x0;\n    PICTURE = 0x1;\n  }\n  optional string title = 0x1;\n  repeated SerialSeason serial_season = 0x2;\n  optional SerialSeasonCoverStyle style = 0x3; \n}\nmessage UgcSeasons {}\nmessage OgvLiveReserve {}\nmessage CombinationEp {}\nmessage Sponsor {}\nmessage ActivityEntranceModule {}\nmessage Relates {}\nmessage Banner {}\nmessage LikeComment {}\nmessage AttentionRecommend {}\nmessage Covenanter {}\nmessage ReplyTab {\n  optional ReplyStyle reply_style = 0x1;\n  optional string title = 0x2;\n  optional TabControl control = 0x3;\n}\nmessage ReplyStyle {\n  optional string badge_url = 0x1;\n  optional string badge_text = 0x2;\n  optional int64 badge_type = 0x3;\n}\nmessage TabControl {\n  optional bool limit = 0x1;\n  optional bool disable = 0x2;\n  optional string disable_click_tip = 0x3;\n}\nmessage ActivityTab {}\n\nmessage Device {\n  optional int32 app_id = 0x1;\n  optional int32 build = 0x2;\n  optional string buvid = 0x3;\n  optional string mobi_app = 0x4;\n  optional string platform = 0x5;\n  optional string device = 0x6;\n  optional string channel = 0x7;\n  optional string brand = 0x8;\n  optional string model = 0x9;\n  optional string osver = 0xa;\n}\n\nmessage DmViewReq {\n  optional int32 pid = 0x1;\n  optional int64 oid = 0x2;\n  optional int32 type = 0x3;\n  optional string spmid = 0x4;\n  optional int32 is_hard_boot = 0x5;\n}\n\nmessage DanmakuFlagConfig {}\n\nmessage VideoMask {}\n\nmessage DanmuPlayerViewConfig {}\n\nmessage UserInfo {}\n\nmessage SubtitleItem {\n  optional int64 id = 0x1;\n  optional string id_str = 0x2;\n  optional string lan = 0x3;\n  optional string lan_doc = 0x4;\n  optional string subtitle_url = 0x5;\n  optional UserInfo author = 0x6;\n  optional int32 type = 0x7;\n  optional string lan_doc_brief = 0x8;\n}\n\nmessage VideoSubtitle {\n  optional string lan = 0x1;\n  optional string lan_doc = 0x2;\n  repeated SubtitleItem subtitles = 0x3;\n}\n\nmessage DmViewReply {\n  optional bool d = 0x1;\n  optional VideoMask mask = 0x2;\n  optional VideoSubtitle subtitle = 0x3;\n  repeated string special_dms = 0x4;\n  optional DanmakuFlagConfig ai_flag = 0x5;\n  optional DanmuPlayerViewConfig player_config = 0x6;\n  optional int32 send_box_style = 0x7;\n  optional bool allow = 0x8;\n  optional bool check_box = 0x9;\n  optional string check_box_show_msg = 0xa;\n  optional string text_placeholder = 0xb;\n  optional string input_place_holder = 0xc;\n  repeated string report_filter_content = 0xd;\n}\n\nmessage Pagination {\n  optional int32 page_size = 0x1;\n  optional string next = 0x2;\n}\n\nmessage PlayerArgs {\n  optional int64 qn = 0x1;\n  optional int64 fnver = 0x2;\n  optional int64 fnval = 0x3;\n  optional int64 force_host = 0x4;\n  optional int64 voice_balance = 0x5;\n}\n\nenum UserType {\n  ALL = 0x0;\n  UP = 0x1;\n  NORMAL_USER = 0x2;\n  AUTHENTICATED_USER = 0x3;\n}\n\nenum UserSort {\n  USER_SORT_DEFAULT = 0x0;\n  USER_SORT_FANS_DESCEND = 0x1;\n  USER_SORT_FANS_ASCEND = 0x2;\n  USER_SORT_LEVEL_DESCEND = 0x3;\n  USER_SORT_LEVEL_ASCEND = 0x4;\n}\n\nenum CategorySort {\n  CATEGORY_SORT_DEFAULT = 0x0;\n  CATEGORY_SORT_PUBLISH_TIME = 0x1;\n  CATEGORY_SORT_CLICK_COUNT = 0x2;\n  CATEGORY_SORT_COMMENT_COUNT = 0x3;\n  CATEGORY_SORT_LIKE_COUNT = 0x4;\n}\n\nmessage SearchByTypeRequest {\n  optional int32 type = 0x1;\n  optional string keyword = 0x2;\n  optional CategorySort category_sort = 0x3;\n  optional int64 category_id = 0x4;\n  optional UserType user_type = 0x5;\n  optional UserSort user_sort = 0x6;\n  optional Pagination pagination = 0x7;\n  optional PlayerArgs player_args = 0x8;\n}\n\nmessage FollowButton {\n  optional string icon = 0x1;\n  map<string, string> texts = 0x2;\n  optional string status_report = 0x3;\n}\n\nmessage CheckMore {\n  optional string content = 0x1;\n  optional string uri = 0x2;\n}\n\nmessage WatchButton {\n  optional string title = 0x1;\n  optional string link = 0x2;\n}\n\nmessage ReasonStyle {\n  optional string text = 0x1;\n  optional string text_color = 0x2;\n  optional string text_color_night = 0x3;\n  optional string bg_color = 0x4;\n  optional string bg_color_night = 0x5;\n  optional string border_color = 0x6;\n  optional string border_color_night = 0x7;\n  optional int32 bg_style = 0x8;\n}\n\nmessage Episode {\n  optional string uri = 0x1;\n  optional string param = 0x2;\n  optional string index = 0x3;\n  repeated ReasonStyle badges = 0x4;\n  optional int32 position = 0x5;\n}\n\nmessage EpisodeNew {\n  optional string title = 0x1;\n  optional string uri = 0x2;\n  optional string param = 0x3;\n  optional int32 is_new = 0x4;\n  repeated ReasonStyle badges = 0x5;\n  optional int32 type = 0x6;\n  optional int32 position = 0x7;\n  optional string cover = 0x8;\n  optional string label = 0x9;\n}\n\nmessage SearchBangumiCard {\n  optional string title = 0x1;\n  optional string cover = 0x2;\n  optional int32 media_type = 0x3;\n  optional int32 play_state = 0x4;\n  optional string area = 0x5;\n  optional string style = 0x6;\n  optional string styles = 0x7;\n  optional string cv = 0x8;\n  optional double rating = 0x9;\n  optional int32 vote = 0xa;\n  optional string target = 0xb;\n  optional string staff = 0xc;\n  optional string prompt = 0xd;\n  optional int64 ptime = 0xe;\n  optional string season_type_name = 0xf;\n  repeated Episode episodes = 0x10;\n  optional int32 is_selection = 0x11;\n  optional int32 is_atten = 0x12;\n  optional string label = 0x13;\n  optional int64 season_id = 0x14;\n  optional string out_name = 0x15;\n  optional string out_icon = 0x16;\n  optional string out_url = 0x17;\n  repeated ReasonStyle badges = 0x18;\n  optional int32 is_out = 0x19;\n  repeated EpisodeNew episodes_new = 0x1a;\n  optional WatchButton watch_button = 0x1b;\n  optional string selection_style = 0x1c;\n  optional CheckMore check_more = 0x1d;\n  optional FollowButton follow_button = 0x1e;\n  optional ReasonStyle style_label = 0x1f;\n  repeated ReasonStyle badges_v2 = 0x20;\n  optional string styles_v2 = 0x21;\n}\n\nmessage SearchItem {\n  optional string uri = 0x1;\n  optional string param = 0x2;\n  optional string goto = 0x3;\n  optional string link_type = 0x4;\n  optional int32 position = 0x5;\n  optional string track_id = 0x6;\n  oneof card_item {\n    SearchBangumiCard bangumi = 0x26;\n  }\n}\n\nmessage PaginationReply {\n  optional string next = 0x1;\n  optional string prev = 0x2;\n}\n\nmessage SearchByTypeResponse {\n  optional string track_id = 0x1;\n  optional int32 pages = 0x2;\n  optional string exp_str = 0x3;\n  optional string keyword = 0x4;\n  optional int32 result_is_recommend = 0x5;\n  repeated SearchItem items = 0x6;\n  optional PaginationReply pagination = 0x7;\n}\n\nmessage SearchNav {\n  optional string name = 0x1;\n  optional int32 total = 0x2;\n  optional int32 pages = 0x3;\n  optional int32 type = 0x4;\n}\n\nmessage FormatDescription {\n  optional int32 quality = 0x1;\n  optional string format = 0x2;\n  optional string description = 0x3;\n  optional string display_desc = 0x4;\n  optional string superscript = 0x5;\n}\n\nmessage VolumeInfo {}\n\nmessage ListenPlayInfo {\n  optional int32 qn = 0x1;\n  optional string format = 0x2;\n  optional int32 qn_type = 0x3;\n  oneof info {\n    ListenPlayDASH play_dash = 0x5;\n  }\n  optional int32 fnver = 0x6;\n  optional int32 fnval = 0x7;\n  repeated FormatDescription formats = 0x8;\n  optional int32 video_codecid = 0x9;\n  optional int64 length = 0xa;\n  optional int32 code = 0xb;\n  optional string message = 0xc;\n  optional int64 expire_time = 0xd;\n  optional VolumeInfo volume = 0xe;\n}\n\nmessage ListenDashSegmentBase {}\n\nmessage ListenDashItem {\n  optional int32 id = 0x1;\n  optional string base_url = 0x2;\n  repeated string backup_url = 0x3;\n  optional int32 bandwidth = 0x4;\n  optional string mime_type = 0x5;\n  optional string codecs = 0x6;\n  optional ListenDashSegmentBase segment_base = 0xc;\n  optional int32 codecid = 0xd;\n  optional string md5 = 0xe;\n  optional int64 size = 0xf;\n}\n\nmessage ListenPlayDASH {\n  optional int32 duration = 0x1;\n  optional float min_buffer_time = 0x2;\n  repeated ListenDashItem audio = 0x3;\n}\n\nmessage EventTracking {}\n\nmessage ListenPlayItem {\n  optional int32 item_type = 0x1;\n  optional int64 oid = 0x3;\n  repeated int64 sub_id = 0x4;\n  optional EventTracking et = 0x5;\n  optional int64 pos = 0x6;\n}\n\nmessage ListenPlayURLReq {\n  optional ListenPlayItem item = 0x1;\n  optional PlayerArgs player_args = 0x2;\n}\n\nmessage ListenPlayURLResp {\n  optional ListenPlayItem item = 0x1;\n  optional int32 playable = 0x2;\n  optional string message = 0x3;\n  map<int64, ListenPlayInfo> player_info = 0x4;\n}\n\nmessage UGCPlayViewReply {\n  optional VideoInfo video_info = 0x1;\n}\n\nmessage BKArcPart {\n  optional int64 oid = 0x1;\n  optional int64 sub_id = 0x2;\n  optional string title = 0x3;\n  optional int64 duration = 0x4;\n  optional int32 page = 0x5;\n}\n"
  },
  {
    "path": "app/src/main/proto/me/iacn/biliroaming/configs.proto",
    "content": "syntax = \"proto3\";\n\npackage me.iacn.biliroaming;\n\noption java_package = \"me.iacn.biliroaming\";\noption java_outer_classname = \"Configs\";\noption optimize_for = LITE_RUNTIME;\n\nmessage Class {\n  optional string name = 1;\n}\n\nmessage Method {\n  optional string name = 1;\n}\n\nmessage Field {\n  optional string name = 1;\n}\n\nmessage Request {\n  optional Class class = 1;\n  optional Field url = 2;\n}\n\nmessage Response {\n  optional Class class = 1;\n  optional Field request = 2;\n}\n\nmessage ResponseBody {\n  optional Class class = 1;\n  optional Method create = 2;\n  optional Method string = 3;\n}\n\nmessage HttpUrl {\n  optional Class class = 1;\n  optional Method parse = 2;\n}\n\nmessage MediaType {\n  optional Class class = 1;\n  optional Method get = 2;\n}\n\nmessage OkHttp {\n  optional Request request = 1;\n  optional Response response = 2;\n  optional ResponseBody response_body = 3;\n  optional MediaType mediaType = 4;\n  optional HttpUrl httpUrl = 5;\n}\n\nmessage FastJson {\n  optional Class class = 1;\n  optional Method parse = 2;\n}\n\nmessage ThemeName {\n  optional Class class = 1;\n  optional Field field = 2;\n}\n\nmessage ReportDownload {\n  optional Class class = 1;\n  optional Method method = 2;\n}\n\nmessage BangumiParams {\n  optional Class class = 1;\n  optional Method params_to_map = 2;\n}\n\nmessage GsonHelper {\n  optional Field gson = 1;\n  optional Class gson_converter = 2;\n  optional Method to_json = 3;\n  optional Method from_json = 4;\n}\n\nmessage PlayerCoreService {\n  optional Class class = 1;\n  optional Method get_playback_speed = 2;\n  optional Method seek_to = 3;\n  optional Method on_seek_complete = 4;\n  optional Class seek_complete_listener = 5;\n  reserved 6, 7, 8;\n}\n\nmessage PcsFacade {\n  optional Class play_speed_manager = 1;\n}\n\nmessage PegasusFeed {\n  optional Class class = 1;\n  optional Method method = 2;\n}\n\nmessage Popular {\n  optional Class class = 1;\n  optional Method method = 2;\n}\n\nmessage OkIO {\n  optional Class class = 1;\n  optional Field field = 2;\n  optional Field length = 3;\n}\n\nmessage OkIOBuffer {\n  optional Class class = 1;\n  optional Method input_stream = 2;\n  optional Method read_from = 3;\n}\n\nmessage DescCopy {\n  repeated Class classes = 1;\n  repeated Method methods = 2;\n}\n\nmessage KanBan {\n  optional Class class = 1;\n  optional Method method = 2;\n}\n\nmessage ToastHelper {\n  optional Class class = 1;\n  optional Method show = 2;\n  optional Method cancel = 3;\n}\n\nmessage ThemeProcessor {\n  optional Class class = 1;\n  repeated Method methods = 2;\n}\n\nmessage MapIds {\n  map<string, int32> ids = 1;\n}\n\nmessage ThemeHelper {\n  optional Class class = 1;\n  optional Field color_array = 2;\n}\n\nmessage ThemeIdHelper {\n  optional Class class = 1;\n  optional Field color_id = 2;\n}\n\nmessage ColumnHelper {\n  optional Class class = 1;\n  optional Field color_array = 2;\n}\n\nmessage SignQuery {\n  optional Class class = 1;\n  optional Method method = 2;\n}\n\nmessage AutoLike {\n  optional KingPositionComponent kingPositionComponent = 1;\n  optional StoryAbsController storyAbsController = 2;\n}\n\nmessage KingPositionComponent {\n  optional Class class = 1;\n  optional Field componentMap = 2;\n}\n\nmessage StoryAbsController {\n  optional Class class = 1;\n  optional Method setMData = 2;\n  optional Method getMPlayer = 3;\n}\n\nmessage Drawer {\n  optional Class class = 1;\n  optional Class layout = 2;\n  optional Class layout_params = 3;\n  optional Method open = 4;\n  optional Method close = 5;\n  optional Method is_open = 6;\n}\n\nmessage DownloadThread {\n  optional Class view_host = 1;\n  optional Class listener = 2;\n  optional Field field = 3;\n  optional ReportDownload report_download = 4;\n}\n\nmessage HomeUserCenter {\n  optional Class class = 1;\n  optional Method add_setting = 2;\n}\n\nmessage Settings {\n  optional Class menu_group_item = 2;\n  optional Class setting_router = 3;\n  repeated HomeUserCenter home_user_center = 5;\n}\n\nmessage BiliAccounts {\n  optional Class class = 1;\n  optional Method get = 2;\n  optional Method get_access_key = 3;\n}\n\nmessage PlayerFullStoryWidget {\n  optional Class class = 1;\n  optional Method method = 2;\n}\n\nmessage BiliCall {\n  optional Class class = 1;\n  optional Class parser = 2;\n  optional Method set_parser = 3;\n  optional Field request = 4;\n}\n\nmessage BiliConfig {\n  optional Class class = 1;\n  optional Method getAppKey = 2;\n}\n\nmessage UpdateInfoSupplier {\n  optional Class class = 1;\n  optional Method check = 2;\n}\n\nmessage PlayerPreloadHolder {\n  optional Class class = 1;\n  optional Method get = 2;\n}\n\nmessage PlayerQualityService {\n  optional Class class = 1;\n  optional Method get_default_qn_thumb = 2;\n}\n\nmessage PlayerSettingHelper {\n  optional Class class = 1;\n  optional Method get_default_qn = 2;\n}\n\nmessage AutoSupremumQuality {\n  optional Class class = 1;\n}\n\nmessage QualityStrategyProvider {\n  optional Class class = 1;\n  optional Method selectQuality = 2;\n}\n\nmessage LiveRtcHelper {\n  optional Class live_rtc_enable_class = 1;\n  optional Method live_rtc_enable_method = 2;\n}\n\nmessage BuiltInThemes {\n  optional Class class = 1;\n  optional Field all = 2;\n}\n\nmessage BiliGlobalPreference {\n  optional Class class = 1;\n  optional Method get = 2;\n}\n\nmessage CardClickProcessor {\n  optional Class class = 1;\n  optional Method on_feed_clicked = 2;\n}\n\nmessage Comment3Copy {\n  optional Class class = 1;\n  optional Method method = 2;\n  optional int32 comment3ViewIndex = 3;\n}\n\nmessage Continuation {\n  optional Class class = 1;\n}\n\nmessage VipQualityTrialService {\n  optional Class class = 1;\n  optional Method canTrial = 2;\n}\n\nmessage LivePlayUrlSelectUtil {\n  optional Class class = 1;\n  optional Method buildSelectorData = 2;\n}\n\nmessage LiveRTCSourceServiceImpl {\n  optional Class class = 1;\n  optional Method switchAuto = 2;\n}\n\nmessage DefaultRequestIntercept {\n  optional Class class = 1;\n  optional Method intercept = 2;\n}\n\nmessage LiveQuality {\n  optional LivePlayUrlSelectUtil selectUtil = 1;\n  optional LiveRTCSourceServiceImpl sourceService = 2;\n  optional DefaultRequestIntercept interceptor = 3;\n}\n\nmessage PreBuiltConfig {\n  optional Class class = 1;\n  optional Method get = 2;\n}\n\nmessage DataSP {\n  optional Class class = 1;\n  optional Method get = 2;\n}\n\nmessage ResolveClientCompanion {\n  optional Class class = 1;\n  optional Method buildCommonResolverParams = 2;\n}\n\nmessage GCommonResolverParams {\n  optional Class class = 1;\n  optional Method setExtraContent = 2;\n}\n\nmessage RewardAd {\n  optional Class class_ = 1;\n  optional Field rewardFlag = 2;\n}\n\nmessage StoryPagerPlayer {\n  optional Class class = 1;\n  optional Method addVideo = 2;\n}\n\nmessage HookInfo {\n  int64 last_update_time = 1;\n  optional MapIds map_ids = 2;\n  int32 client_version_code = 3;\n  int32 module_version_code = 4;\n  string module_version_name = 5;\n  optional Class retrofit_response = 10;\n  optional OkHttp ok_http = 11;\n  optional FastJson fast_json = 12;\n  optional ThemeHelper theme_helper = 13;\n  optional ThemeIdHelper theme_id_helper = 14;\n  optional Class bangumi_api_response = 15;\n  optional BiliAccounts bili_accounts = 16;\n  optional ColumnHelper column_helper = 17;\n  optional Method skin_list = 19;\n  optional ThemeProcessor theme_processor = 20;\n  optional Class theme_list_click = 22;\n  optional ThemeName theme_name = 24;\n  optional AutoLike autoLike = 25;\n  optional SignQuery sign_query = 27;\n  optional Class general_response = 28;\n  optional Drawer drawer = 30;\n  optional Settings settings = 31;\n  optional DownloadThread download_thread = 34;\n  optional BangumiParams bangumi_params = 45;\n  optional GsonHelper gson_helper = 46;\n  optional PlayerCoreService player_core_service = 47;\n  optional PegasusFeed pegasus_feed = 49;\n  optional Popular popular = 50;\n  optional Class chronos_switch = 51;\n  optional Class subtitle_span = 52;\n  optional Class comment_long_click = 53;\n  optional OkIO okio = 54;\n  optional OkIOBuffer okio_buffer = 55;\n  optional Class comment_span = 57;\n  repeated Class dyn_desc_holder_listener = 58;\n  optional DescCopy desc_copy = 59;\n  optional Class bangumi_season = 60;\n  optional KanBan kan_ban = 67;\n  optional ToastHelper toast_helper = 68;\n  optional Class brotli_input_stream = 69;\n  repeated PlayerFullStoryWidget player_full_story_widget = 71;\n  optional BiliCall bili_call = 72;\n  optional Class comment_long_click_new = 73;\n  optional Method on_operate_click = 74;\n  optional Method get_content_string = 75;\n  optional Class live_pager_recycler_view = 76;\n  optional BiliConfig bili_config = 77;\n  optional UpdateInfoSupplier update_info_supplier = 78;\n  optional Method bangumi_season_activity_entrance = 82;\n  optional PlayerPreloadHolder player_preload_holder = 83;\n  optional PlayerSettingHelper player_setting_helper = 84;\n  optional LiveRtcHelper live_rtc_helper = 85;\n  optional Class theme_colors = 86;\n  optional BuiltInThemes built_in_themes = 87;\n  optional BiliGlobalPreference bili_global_preference = 88;\n  optional CardClickProcessor card_click_processor = 89;\n  optional Class publish_to_following_config = 90;\n  optional Comment3Copy comment3_copy = 91;\n  repeated PlayerQualityService player_quality_service = 92;\n  optional Class play_speed_manager = 93;\n  optional AutoSupremumQuality autoSupremumQuality = 94;\n  optional QualityStrategyProvider qualityStrategyProvider = 95;\n  optional Continuation continuation = 96;\n  optional VipQualityTrialService vipQualityTrialService = 97;\n  optional LiveQuality liveQuality = 98;\n  optional PreBuiltConfig preBuiltConfig = 99;\n  optional DataSP dataSP = 100;\n  optional Class pegasus_parser = 101;\n  optional ResolveClientCompanion resolveClientCompanion = 102;\n  optional GCommonResolverParams gCommonResolverParams = 103;\n  optional RewardAd rewardAd = 104;\n  optional StoryPagerPlayer storyPagerPlayer = 105;\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_clear.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"#3F51B5\"\n        android:pathData=\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<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.11\"\n        android:scaleY=\"0.11\"\n        android:translateX=\"20.5\"\n        android:translateY=\"16.3\">\n        <path\n            android:fillColor=\"#ff731e2b\"\n            android:pathData=\"M219.39 90.38c17.42-11.5 38.59-18.75 59.64-16.15 17.48 1.97 34.12 10.82 45.22 24.51 7.73 9.07 12.3 20.24 15.4 31.62 11.77 0.38 23.58 0.52 35.25 2.18 19.67 2.93 39.2 7.83 57.26 16.28 23.2 10.52 44.12 25.97 61.05 44.98 23.16 26.24 39.35 58.6 46.35 92.9 3.05 13.21 3.51 26.8 3.83 40.3-0.03 6.92-0.75 13.81-0.68 20.73 4.26-1.73 8.6-3.55 13.26-3.84 10.19-0.82 20.36 3.49 27.64 10.5 6.53 5.87 9.98 14.3 11.4 22.82-3.71-4-6.93-8.78-12.01-11.18-7.38-3.42-15.94-3.7-23.8-1.98-6.34 1.36-11.32 5.72-16.24 9.64 6.29-0.76 12.67-1.69 19-0.72 7.6 0.92 14.73 4.54 20.28 9.75 4.09 3.99 5.92 9.64 7.19 15.08 1.82 7.93 1.56 16.75-2.77 23.86-4.92 8.22-12.43 15.15-21.48 18.51-4.13 1.66-8.57 0.52-12.71-0.41l-0.14-2.25c3.35-1.39 7.42-2.55 9.03-6.17 3.21-6.9 2.35-15.57-1.87-21.86-4.79-5.43-11.72-8.48-18.57-10.42 1.91 3.73 4.22 7.27 5.76 11.18 4.76 14.04 4.89 29.23 2.23 43.71-3.59 22.91-11.77 44.9-14.08 68.03-1.42 17.57 1.28 35.34 7.28 51.88 4.17 11.55 9.9 22.44 15.79 33.19 5.06 9.48 9.5 19.99 8.39 30.98-1.26 13.65-10.3 26.27-22.76 31.97h-11.62c3.73-3.68 8.48-7.42 8.96-13.05 1.3-11.39-1.94-22.79-6.62-33.08-2.16-5.11-5.42-9.66-9.28-13.61-4.35 7.02-8.27 14.6-14.88 19.82-5.58 4.97-12.68 7.79-20 8.96 3.95-4.21 8.67-8.39 9.7-14.38 1.7-9.03-0.29-18.26-2.58-27-3.23-10.76-6.37-21.78-12.7-31.21-8.61-12.85-15.55-27.01-18.77-42.22-2.29-9.18-2.6-18.67-3-28.07-8.48 4.05-17.38 7.06-26.11 10.49-7.67 2.98-10.49 11.53-16.58 16.39-2.71 1.93-5.92-0.2-7.56-2.48-7.42-9.56-10.77-21.43-14.36-32.79-9.23 3.43-18.88 5.41-28.45 7.58 5.84 4.4 13.22 6.31 18.6 11.36 5.83 5.2 10.93 11.14 15.78 17.24 2.08 2.76 5.1 4.54 8.26 5.82 6.42 2.67 12.55 6.38 17.18 11.64 2.3 2.6 2.1 6.38 1.63 9.59-0.66 4.2-3.41 7.7-4.26 11.83 0.79 4.46 4.23 8.23 4.01 12.9-1.39 2.65-3.91 4.42-6.06 6.42l33.4 41.63c5.21 6.38 10.2 12.93 15.7 19.07l8.28-3.48c7.58 9.79 13.58 20.7 20.99 30.63h-4.4c-5.96-8.74-11.44-17.79-16.87-26.86-8.2 3.56-15.64 8.61-22.94 13.71-5.96 3.73-11.19 8.42-16.36 13.15h-6.05l-46.91-67.45c-2.35 0.69-4.7 1.8-7.19 1.7-1.61-0.82-2.97-2.02-4.39-3.09 0.57 7.94 0.44 16.29 4.17 23.56l12.94 27.03c2.93 6.1 5.45 12.42 8.92 18.25h-4.63c-4.35-9.27-8.14-18.79-12.6-28-9.52 3.59-19.13 6.91-28.72 10.29 2.04 5.9 3.74 11.93 6.12 17.71h-2.76c-1.79-5.7-3.34-11.49-5.54-17.05-9.92 2.64-19.59 6.89-29.93 7.45-3.82-3.99-5.33-9.53-7.96-14.29-3.27-8.64-6.4-17.39-10.77-25.55-0.45 5.38-0.11 10.79 1.09 16.06 1.45 11.08 0.88 22.38 3.2 33.38h-2.05l-1.57-21.53c-3.15 1.27-6.39 2.29-9.69 3.1 1.15 6.12 1.78 12.33 3.04 18.43h-2.06c-0.77-5.37-1.13-10.81-2.24-16.12-0.84-4.21-3.51-7.68-5.62-11.31 3.06-4.43 3.39-9.86 2.14-14.96-3.52 6.44-5.88 13.41-8.73 20.16-2.08 4.91-4.18 9.82-6 14.85-3-1.11-5.94-2.37-8.85-3.69l-0.26 11.07h-1.68l0.23-12.04-8.32-3.96c-0.29 5.33-0.33 10.66-0.17 16h-3.65l0.13-82c0.03-7.05 0.39-14.08 0.57-21.12l2.66 1.75c0.16 3.75 0.22 7.52 0.19 11.29-0.14 7.51-0.16 15.02 0.05 22.53l11.47-0.36c-0.84-11.42-0.78-22.88-0.37-34.31 0.51-9.3 0.98-18.61 2.07-27.87l-5.67-0.47c-0.1-10.33 1.75-20.55 4.36-30.51 0.66-2.68 1.22-5.38 1.81-8.07-4.11-1.23-8.32-2.07-12.52-2.9-14.9-3.93-29.95-7.6-44.09-13.85l-0.28 7.69c-2.01 14.3-7.42 27.84-12.78 41.16l-3.27 4.96c3.45 5.09 6.8 10.24 10.43 15.2 2.94 4.07 6.76 8.52 12.21 8.67 6.98 0.04 13.42-3.16 20.24-4.18 3.2-0.49 6.39 0.25 9.5 0.91-1.1-3.12-2.64-6.46-1.75-9.83 3.34 5.45 5.57 11.51 6.68 17.79a123.77 123.77 0 0 1-3.77-1.53c-3.15-1.94-6.44-4.72-10.4-4.15-8.18 1.08-16.01 5.6-24.44 4.02-5.79-1.05-8.76-6.7-11.96-11.01-4.31-5.7-7.59-12.17-12.66-17.26-4.92 18-10.75 35.88-19.69 52.34-5.47 9.55-6.38 20.79-7.61 31.5-1.04 7.85 0.15 15.79 2.7 23.25-5.75-3.22-11.68-6.47-16-11.58-4.4-4.81-6.38-11.14-8.29-17.22-6.51 10.16-11.55 21.52-13.31 33.52-1.24 7.54 1.1 15.11 4.77 21.64h-6.63c-8.19-2.7-15.1-8.61-19.3-16.12-7.71-13.75-8.39-30.65-3.95-45.57 4.23-15.01 13.14-28.08 18.33-42.72 7.63-21.08 8.02-43.98 5.78-66.05l-24.7-14.26c-2.4-1.27-4.92-3.31-7.81-2.49-5.93 1.3-11.63 3.91-17.8 3.97-8.29 0.48-17.36-2.51-22.34-9.46-3.74-5.22-2.29-11.94-1.74-17.86-2.93-1.28-6.82-1.93-8.16-5.24-2.03-4.77-1.85-10.14-1.43-15.19 0.73-7.7 6.47-14.64 13.9-16.77 3.98-1.15 8.03 0.38 11.88 1.34a84.07 84.07 0 0 1-8.71-24.95c-0.66-4.12-1.44-8.75 0.72-12.57 4.14-5.71 13.66-8.25 18.89-2.54 0.28-4.37 0.62-8.88 2.66-12.83 5.13-9.73 13.95-16.87 23.66-21.73-3.12-1.6-6.46-3.83-10.12-2.86-6.96 1.43-12.21 7.07-14.36 13.68l-2.36 0.16c-0.52-5.75-1.25-11.64 0.21-17.31 1.96-7.78 6.72-15.17 13.89-19.06 4.07-2.25 8.85-2.3 13.36-2.72 0.88-5.82 1.6-11.66 2.69-17.43 6.54-31.77 23.44-60.74 45.47-84.28 5.77-5.99 10.04-13.21 15.65-19.34 5.56-6.75 12.07-12.6 18.32-18.68 17.67-17.76 39.21-31.7 62.77-40.25 18.45-6.92 38.07-10.05 57.68-11.13-2.49-2.67-5.22-5.19-8.54-6.79-14.41-7.32-30.05-12.21-46.05-14.56-9.42-0.99-18.95-2.25-28.43-1.15-9.21 0.78-18.51 2.38-26.91 6.43-5.3 2.41-9.87 6.07-14.49 9.54 6.21-13.86 17.61-24.74 30.25-32.83m18.73-6.28c-12.73 5.36-24.87 12.54-34.78 22.25-2.6 2.51-4.79 5.39-6.75 8.42 4.94-2.58 9.79-5.5 15.25-6.88 3.28-1 6.88-1.25 9.87-3.04 5.78 0.09 11.51-0.76 17.28-1.04 26.84 0.4 53.97 6.98 76.83 21.35 6.13 3.65 11.48 8.55 15.65 14.33 2.4 3.55 4.23 7.45 6.67 10.98-0.02-16.3-4.67-32.93-14.49-46.11-8.29-11.45-20.4-20.07-33.98-24.06-3.62-0.96-7.34-2.48-11.13-1.91-3.14-0.77-6.35-1.27-9.58-1.09-10.56 0.53-21.09 2.67-30.84 6.8m55.59 51.77c-4.06-0.06-8 1.08-11.96 1.8-26.63 5.27-51.75 17.31-73.16 33.92-1.86 1.43-3.67 2.99-5.01 4.95-5.96 3.92-10.55 9.47-15.5 14.53-2.9 2.92-4.75 6.62-7.09 9.95-4.71 5.6-11.12 9.51-15.7 15.26-6.1 7.86-13.35 14.81-18.79 23.18-14.98 22.43-26.31 47.71-30.21 74.54-0.57 3.66-0.74 7.37-0.5 11.07-1.79 6.77-0.69 13.85-0.48 20.75 0.5 3.59 0.1 7.57 1.94 10.82-0.17 8.49 2.85 16.58 5.44 24.54 2.69 8.49 8.07 15.71 13.58 22.56 2.36 2.96 5.45 5.2 8.58 7.27-1.69-4.95-3.88-9.83-4.34-15.11-0.36-4.43-0.06-8.88-0.18-13.32 0.93 0.72 1.79 1.53 2.72 2.27 2.64 7.08 8.64 12.21 14.59 16.53 3.58 2.54 8.03 3.83 11.03 7.16l-4.85 0.31c-3.37-2.16-6.85-4.19-10.01-6.65-3.73-2.91-6.65-6.66-9.7-10.23-0.12 4.72-0.25 9.68 1.93 14.02 1.77 3.41 3.79 6.69 5.37 10.21-6.44-1.09-11.42-5.47-16.41-9.33-2.95 8.05-3.5 16.68-4.25 25.14 0.06 2.52-0.85 5.97 1.91 7.4 8.73 5.8 20.13 4.67 28.95 10.4-0.15 1.88-0.83 3.85-0.27 5.73 3.27 2.25 7.05 3.63 10.49 5.61 4.65 2.64 9.33 5.31 14.42 7.03-4.85-5.35-10.56-10.12-13.68-16.8-4.53-8-4.87-17.71-2.38-26.42 1.6 0.41 3.24 0.64 4.89 0.72-3.53 7.67-2.89 16.72 1.18 24.04 6.52 12.07 18.79 19.54 30.88 25.12 14.83 6.08 29.29 13.47 45.12 16.66 8.91 1.68 17.61 4.45 26.66 5.36 5.73 0.75 11.53 0.39 17.27 1 12.15 1.25 24.36 0.04 36.51-0.64 16.2-2.05 32.45-4.24 48.25-8.46 8.22-2.78 16.09-6.49 24.22-9.53 9.63-3.85 18.49-9.32 27.27-14.76 4.99-3.28 11.1-0.74 16.6-1.34 5.16-0.13 9.87-2.8 13.23-6.61 3.35-2.32 5.18-6.16 7.1-9.63-7.27 2.51-14.37 5.77-22.11 6.52-6.89 1.26-13.85 0.24-20.77 0.1-3.2-1.68-6.45-3.29-9.79-4.64l2.12-0.49c5.8 2.2 12.08 2.32 18.21 2.36 8.75-0.01 17.46-1.92 25.56-5.19 5.58-2.36 11.25-5.07 15.33-9.71 7.86-4.45 12.1-12.65 17.04-19.82-1.65 7.18-6.31 13.28-7.29 20.67l-0.68 0.14c-1.44 2.47-2.8 4.98-4.04 7.55 11.92-4.16 23.72-10.73 31.21-21.17l0.87-1.15c5.05-4.67 7.97-11.02 11.03-17.05 4.9-10.72 7.88-22.29 9.16-34 1.93-19.07 0.6-38.55-4.1-57.14-1.16-8.07-4.22-15.72-6.44-23.53-1.56-5.05-2.46-10.36-4.95-15.07l-1.59-8.42c10.62 18.95 15.84 40.26 20.5 61.3 0.95-11.9-0.17-23.82-2.11-35.56-1.29-12.17-5.77-23.67-10.05-35.04-11.04-29.2-28.88-56.03-52.44-76.61-6.82-5.97-13.94-11.86-22.1-15.89-7.22-5.49-15.48-9.44-23.64-13.32-15.86-7.47-32.96-11.83-50.15-14.89-12.52-2.23-25.28-1.08-37.91-1.41 0.31 6.93 0.56 13.92-0.5 20.8-1.12-1.5-2.29-2.97-3.67-4.25-1.92-6.89-7.39-11.84-12.53-16.45-2.27-2.19-5.67-1.55-8.53-1.58-7.12 0.45-14.3 0.42-21.3 1.92M89.63 337.62c-2.74 3.96-3.12 9.02-3.84 13.66 2.43-2.06 4.7-4.34 7.38-6.09 3.12-2.04 6.86-2.68 10.43-3.51l6.59 1.25c0.23-5.97 0.22-11.94 0.28-17.9l-2.11-0.54c-8.18 0.18-15.92 5.38-18.73 13.13m452.41 15.39c-1.76 5.39-1.51 11.19-2.12 16.77 5.37-3.64 11.17-6.74 17.47-8.43 5.8-1.77 11.93-1.6 17.93-1.76 3.83 1.72 7.77 3.17 11.78 4.45-5.43-8.34-14.28-14.25-23.96-16.32-7.29-0.91-15.45 0.32-21.1 5.29m-433.99 1.03c-6.12 2.27-10.78 7.21-14.98 12.03-2.76 3.53-6.27 7.1-6.36 11.88-0.41 7.31 0.6 15.04 4.67 21.29 1.49-5.07 1.98-10.56 4.72-15.18 2.2-3.1 6.51-2.61 9.84-2.81 4.03-1.02 7.39-3.49 10.77-5.76-2.37-7.57-3.73-15.38-5.6-23.08-1 0.57-2.02 1.11-3.06 1.63m431.19 23.34c-1.5 5.49-2.67 11.06-4.05 16.58 4.62 1.62 9.41 2.82 13.87 4.87 3.77 1.88 7.31 4.19 11.11 6.01 3.17 3.54 7.56 5.74 10.77 9.27 2.31 2.4 3.23 5.7 4.29 8.78-0.25 2.32-0.72 4.72-2.42 6.46-0.39-2.92-0.47-5.92-1.41-8.73-1.01-2.02-2.8-3.47-4.31-5.08-0.31 4.45-0.02 9.02-1.21 13.37-1.19 4.32-4.87 7.17-7.83 10.26 7.55-0.61 13.73-5.46 18.97-10.54 5.84-5.69 10.33-13.23 10.31-21.6-0.2-7.75-1.47-16.39-7.41-21.96-7.98-8.19-20.15-10.1-31.09-9.17l-9.59 1.48m-426.88 5.71c2.46 9.71-2.06 19.17-1.6 28.89 1.9-1.85 3.67-3.85 5.83-5.4 2.27-1.71 5.36-1.61 8.01-1.02 2.65 0.47 3.82 3.16 5.25 5.11 0.6-2.55 1.45-5.06 1.64-7.68-0.35-1.84-1.66-3.31-2.61-4.86-3.96-5.5-6.98-11.58-10.13-17.56a110.05 110.05 0 0 1-6.39 2.52m-45.56 6.02c-0.56 10.38 3.52 20.45 7.75 29.76 5.12-2.37 10.67-3.84 16.35-3.46-2.81-7.76-6.89-15.18-7.89-23.49-0.5-2.08-0.33-4.49-1.65-6.26-4.47-3.92-11.62-0.99-14.56 3.45m29.82 2.48c-1.99 7.81-2.79 16.03-1.83 24.06 4.27-0.82 8.63-1.14 12.97-0.87-0.28-5.75-0.59-11.56 0.49-17.25 0.49-3.36 1.51-6.68 1.35-10.1-1.65-2.82-5.58-3.28-8.47-2.49-2.97 0.74-3.63 4.17-4.51 6.65m-8.56 7.44c0.38 3.05 0.94 6.34 3.67 8.22-0.89-2.88-2.18-5.61-3.67-8.22m445.46 0.05c-3.47 8.28-7.25 16.54-12.79 23.66-9.01 12.03-22.32 21.27-37.36 23.5-3.33 0.95-8.33 0.57-9.58 4.61-2.77 7-3.98 14.49-5.21 21.88-2.78 17.35-3.05 35.48 2.12 52.43 2.5 15.8 12 29.12 18.97 43.17 7.19 13.32 11.48 28.03 14.11 42.87 1.33 6.77 0.06 13.61-1.68 20.18 7.21-4.4 13.54-10.75 16.54-18.77 4.04-10.3 3.01-21.71 1.04-32.35 0.4-1.54 0.84-3.07 1.12-4.63 1.01 5.12 2.6 10.14 2.99 15.37 0.46 3.8 0.15 7.71 0.98 11.47 1.06 2.36 2.88 4.26 4.38 6.36 8.02 10.91 13.38 23.63 16.03 36.89-0.27 7.35 0 15.3-4.75 21.45 2.81-1.5 5.58-3.06 8.3-4.73 7.85-6.92 13.83-16.7 13.77-27.43 0.41-8.7-3.22-16.83-7.11-24.39-4.41-8.67-9.05-17.24-12.91-26.17-5.79-13.33-9.23-27.69-10.02-42.2-0.39-6.44-2.21-12.75-1.94-19.23 0.39-11.17 3.69-21.94 5.51-32.89 3.88-13.71 7.35-27.59 9.5-41.68 1.84-14.59 1.67-30.23-5.43-43.5-1.37-2.74-4.11-4.29-6.58-5.87m-415.37 11.11c-2.75 1.71-3.67 4.91-4.82 7.73l10.81 3.42c1.37-3.62 1.77-7.49 1.63-11.33-2.49-0.29-5.38-1.55-7.62 0.18m9.2 20.93c0.62-5.4 1.1-10.81 1.39-16.23-1.43 5.29-1.74 10.77-1.39 16.23m-31.33-13.43l2.18 5.67 0.77-0.02 1.07 3.4c-1.35-0.64-2.65-1.39-4.07-1.86l-3.88-6.41c-5.08 0.22-11.36-0.52-15.15 3.56 0.67 1.55 1.51 3.02 2.28 4.53l0.82-0.15 0.89 4.13-3.3 0.2 0.05-0.8-6.89-3.93c-0.87 5.37-1.35 11.16 0.99 16.25 1.5 2.34 4.01 3.76 6.22 5.34 1.43 2.99 3.08 6.01 3.3 9.39 0.04 2.54-0.74 4.99-1.29 7.44l-4.81 2.82c0.08 5.4 0.64 11.72 5.16 15.37 1.95 0.52 3.94-0.15 5.91-0.08 6.21-0.97 13.01-4.08 19.01-0.77 9.24 4.61 18.08 9.97 27.15 14.89 1.72 0.76 3.48 1.46 5.15 2.34 4.32-6.85 7.33-14.39 10.7-21.72 3.16-7.26 6.77-14.37 9.08-21.96 0.58-1.5 0.24-3.1-0.26-4.56-5.51-1.29-10.88-3.11-16.36-4.51-4.36-1.34-8.88-2.68-12.61-5.38-3.29-2.43-4.06-6.65-5.02-10.36l0.43-5.78c-3.37-1.85-6.99-3.21-10.71-4.16l-0.4 3.79 1.61 0.93-1.36 1.57-1.76-2.74c-0.64-1.67-1.06-3.45-2.03-4.95-3.92-2.12-8.59-1.53-12.87-1.48m-37.69 9.62c-8.35 5.13-9.37 16.92-5.98 25.31 1.69 4.25 6.75 3.01 10.23 2.38 0.17-2.55 0.14-5.16 0.88-7.62 0.92-2.18 2.81-3.73 4.34-5.47-0.87-5.59-0.69-11.26 0.12-16.84-3.28 0.18-6.82 0.18-9.59 2.24M476.85 445c2.75-2.09 4.91-4.85 6.13-8.1-3.38 1.5-5.52 4.48-6.13 8.1m-410.71 1.87c-0.61 3.5-1.11 7.02-1.64 10.54 3.3 2.14 6.06 6.45 10.41 5.98 4.86-1.31 3.85-7.96 2.14-11.47-1.65-4.41-6.57-6.09-10.91-5.05m395.07 16.41l-5.41 2.65 8.59 11.27 2.21-16.76-0.69 0.39c0.84-1.21 1.43-2.57 2.08-3.89-2.64 1.67-4.95 3.81-6.78 6.34m-302.03-4.41c-4.37 12.22-10.05 23.89-14.97 35.88 11.19 6.13 23.54 9.72 35.15 14.93 3.35 1.38 6.8 2.48 10.3 3.38l10.55-27.9c0.29-1.56 1.37-3.26 0.66-4.8-2.25-1.81-4.96-2.91-7.52-4.18l-34.17-17.31m-98.87 2.42c-2.5 3.88-1.62 9.05 0.53 12.89 3.02 4.92 8.63 7.43 14.13 8.39-1.42-4.07-2.42-8.28-2.73-12.59-0.17-3.94-3.7-6.51-6.63-8.59-1.53-0.85-4.3-2.34-5.3-0.1m376.28 6.73c-2.07-0.55-3.85 1.51-5.61 2.4-1.73 1.2-3.48 2.35-5.24 3.49l-4.48 2.79c-2.45 1.55-4.95 3.03-7.43 4.52l-5.73 2.33c-2.36 1.07-5.28 1.51-7.04 3.51 2.64 9.36 7.15 18.08 11.31 26.84 1.7 2.16 3.51 4.24 5.41 6.24 2.5-2.9 4.91-5.89 7.29-8.89l4.32-5.11c2.21-1.26 4.48-2.4 6.79-3.45l6.99-2.58 8.21-3.07c1.9-0.85 3.81-1.67 5.76-2.42 1.97-1.18 4.61-1.78 5.9-3.78 1.62-5.22-1.47-10.05-3.73-14.51-2.4-3.12-4.37-6.76-7.69-9-5-0.56-10.02 0.53-15.03 0.69m-233.01 12.03c-1.44 8.07-3.92 15.92-7.02 23.49-0.86 2.27-1.94 4.56-1.99 7.03-3.28 6.18-5.48 12.85-7.71 19.47 2.64 1.21 5.35 2.26 8.15 3.06l0.01 0.87 3.9 0.03c5.87-10.37 9.71-21.76 12.93-33.19 1.35-5.44 3.06-11.06 2.3-16.72-2.33-3.27-6.99-3.15-10.57-4.04m150 18.4a144.46 144.46 0 0 0-2.92 15.72c-2.19 8.9-2.02 18.15-3.3 27.2-0.37 0.02-1.09 0.08-1.46 0.11-5.99-0.19-11.99 0.38-17.98-0.01-12.85-0.72-25.7-1.79-38.58-1.82l-4.57-0.03-9.97 0.12c-1.97 12.6-2.22 25.41-2.02 38.14-0.15 6.38-0.34 12.77-0.06 19.15 0.2 1.66-0.02 3.7 1.44 4.89 2.98 2.85 6.64 4.87 10.16 6.99 1.09 3.18 4.06 5.19 6.5 7.3 1.91 3.96 5.64 6.7 8.38 10.09 1.16-1.4 3-2.38 3.59-4.16 1.74-1.55 4.6-2.64 5.16-5.03 4.7-2.07 8.42-5.66 12.51-8.67l5.54-3.73c5.11-0.37 10.24-0.39 15.37-0.53-0.21-7.95-1.34-15.85-1.24-23.81-0.2-12.3-1.99-24.72 0-36.96l0.84 3.38c0.22 19.12 2.01 38.17 2.46 57.27 10.78 1.26 21.42 3.42 31.75 6.82-0.47-5.46 0.61-11.07-0.62-16.41-1.74-4.99-7.67-4.09-11.75-5.37-3.03-0.52-3.48-4.05-5.01-6.18-0.37-7.64-2.87-15.69 0.2-23.06 0.99 2.23 1.56 4.65 1.44 7.12-0.08 6.19 0.23 12.71 3.14 18.32 3.62 1.36 7.48 1.92 11.23 2.81 3.19 0.89 6.86-0.69 8.8-3.25 5.77-1.27 10.69-4.65 15.88-7.28 3.17-1.61 6.23-3.45 8.94-5.76 4.11-1.18 7.42-4.15 10.97-6.43 6.64-4.21 9.22-12.6 9.25-20.06-2.2-3.6-5.41-6.98-9.45-8.43-3.49-2.79-7.58-4.63-11.49-6.72-4.25-2.26-6.68-6.61-9.65-10.2-4.18-5.5-9.68-9.79-15.3-13.7-5.58-3.49-11.81-5.78-17.89-8.19-3.33-1.5-6.93-0.34-10.29 0.36m-11.9 3.25c-13.02 10.99-31.67 14.95-47.92 9.59-7.1-1.54-12.19-6.92-18.24-10.47l-1.82 9.08c-2.27 8.96-3.37 18.19-3.51 27.43 8.96 0.72 17.86-1.36 26.81-0.53 5.65 0.59 11.32 0.47 16.99 0.74 10.12 1.11 20.32 1.51 30.5 1.57 1.12-7.64 2.25-15.32 2.69-23.03 1.21-5.86 2.06-11.79 3.58-17.59-3.31-0.03-6.59 0.97-9.08 3.21m-207.51-0.11c0.82 10.55 1.35 21.11 1.03 31.7-0.77 7.86-1.35 15.8-3.45 23.45-0.65 2.38-1.34 4.77-1.55 7.26-2.53 4.74-4.06 9.92-6.4 14.75-3.48 7.83-7.81 15.28-10.67 23.37-2.55 7.37-5.12 14.95-4.88 22.87-0.59 12.42 3.13 25.48 11.9 34.56 3.22 3.75 7.09 6.89 11.67 8.81-4.92-10.34-1.5-21.84-0.06-32.56 3.17-6.08 4.68-12.89 8.09-18.88 1.94-4.25 5.96-7.85 5.51-12.88-0.22-7.77 0.9-15.5 2.64-23.05 0.94-4.15 2.6-8.12 3.27-12.33l1.87 1.14c-4.12 13.54-5.97 28.05-4.08 42.13 1.25 10.14 7.09 19.21 15.02 25.47-1.4-16.24 1.26-32.77 7.49-47.82 7.24-17.84 14.89-35.61 19.37-54.4l-7.88-3.43c1.28-4.72 2.78-9.37 4.5-13.95-6.96-3.93-14.71-6.07-22-9.29-7.58-2.94-14.97-6.34-22.45-9.52-2.56 2.04-5.66 3.06-8.94 2.6m174.83 0.23c-8.5 0.65-17-0.17-25.5 0.21 6.21 5.96 15.28 7.07 23.45 8.08 10.43 0.18 21.34-2.65 29.63-9.19-9.2-0.04-18.37 1.02-27.58 0.9m109.08 68.07c-7.98 4.29-15.39 9.57-23.53 13.6-5.91 3.29-12.24 5.77-18.03 9.29 2.01 3.29 4.29 6.41 6.72 9.4 1.76-0.84 3.72-1.48 5.05-2.98 6.23-1.98 11.86-5.36 17.7-8.22 8.13-4.08 15.82-9.19 22.04-15.89l-4.36-9.45c-1.99 1.23-4.09 2.41-5.59 4.25m-25.1 30.67c4.86 8.26 11.04 15.65 16.56 23.47 10.12 14.67 19.81 29.67 30.69 43.8 8.7-9 18.96-16.3 29.79-22.5-1.92-3.13-4.07-6.09-6.57-8.77l-27.55-34.58c-4.41-5.96-9.14-11.67-13.25-17.84-8.69 7.48-19.49 11.48-29.67 16.42m-133.19 4.3c-0.21 13.15-0.25 26.32 0.3 39.46-0.1 2.38-0.58 4.89 0.24 7.2 5.41 2.85 11.33 4.6 16.71 7.55 5.62-11.9 9.84-24.43 15.81-36.18-4.54-5.35-8.99-10.84-14.22-15.55-4.13-3.91-10.25-2.68-15.4-2.63l-3.44 0.15m63.59 4.5c-5.04 3.26-10.07 6.63-14.38 10.84l2 0.88c1.68 5.63 4.47 10.83 6.67 16.27 3.15 7.12 5.49 14.73 10.35 20.96 15.87-3.78 31.13-9.63 46.62-14.67 3.88-1.22 7.68-2.72 11.3-4.61l-1.96-1.94c-2.02-8.15-7.92-14.8-8.95-23.27-7.97-2.97-16.25-5.54-24.77-6.14-5.69-1.24-11.5-2.06-17.33-1.96-3.54-0.06-6.66 1.82-9.55 3.64M297.7 629.7l-2.49-3.86 0.57 8.58c3.46-0.33 6.95-0.33 10.41-0.01-0.45-3.59-0.72-7.2-1.09-10.79-2.57 1.9-4.98 3.99-7.4 6.08m-0.28 6.72c-6.1 2.24-3.73 11.29 1.93 12.37 5.71 2 10.46-5.8 7.43-10.49-1.73-3.38-6.35-3.17-9.36-1.88zM422.79 254.2c0.68-2.68 2.36-4.82 5.22-5.23-0.33 2.89-2.51 4.6-5.22 5.23zm6.74-0.17c1.14-1.84 2.97-3.03 4.78-4.14-0.11 2.26-2.16 6.89-4.78 4.14z\" />\n\n        <path\n            android:fillColor=\"#fffeeddc\"\n            android:pathData=\"M238.12 84.1c9.75-4.13 20.28-6.27 30.84-6.8 3.23-0.18 6.44 0.32 9.58 1.09-20.11 5.84-40.27 13.28-56.83 26.46-2.99 1.79-6.59 2.04-9.87 3.04-5.46 1.38-10.31 4.3-15.25 6.88 1.96-3.03 4.15-5.91 6.75-8.42 9.91-9.71 22.05-16.89 34.78-22.25zm-50.04 106.97c4.95-5.06 9.54-10.61 15.5-14.53-2.94 4.92-7.01 8.95-11 13-6.26 6.32-9.17 14.9-13.06 22.7 2.53 2.09 6.13 4.11 6.08 7.86-0.17 5.79-3.52 11.07-7.36 15.21-3.02 3.33-8.2 2.38-11.44-0.08-3.31-2.27-2.63-6.85-3.29-10.32-8.04 9.99-15.72 20.3-22.43 31.23-8.41 13.36-15.9 27.48-20.39 42.67-2.58 8.58-1.89 17.77-4.9 26.26-0.24-3.7-0.07-7.41 0.5-11.07 3.9-26.83 15.23-52.11 30.21-74.54 5.44-8.37 12.69-15.32 18.79-23.18 4.58-5.75 10.99-9.66 15.7-15.26 2.34-3.33 4.19-7.03 7.09-9.95zM89.63 337.62c2.81-7.75 10.55-12.95 18.73-13.13l0.62 5.35-3.21 0.89c-5.69 1.46-11.3 3.47-16.14 6.89z\" />\n\n        <path\n            android:fillColor=\"#fff4a2c5\"\n            android:pathData=\"M278.54 78.39c3.79-0.57 7.51 0.95 11.13 1.91 13.58 3.99 25.69 12.61 33.98 24.06 9.82 13.18 14.47 29.81 14.49 46.11-2.44-3.53-4.27-7.43-6.67-10.98-4.17-5.78-9.52-10.68-15.65-14.33-22.86-14.37-49.99-20.95-76.83-21.35-5.77 0.28-11.5 1.13-17.28 1.04 16.56-13.18 36.72-20.62 56.83-26.46zm61.7 57.04c12.63 0.33 25.39-0.82 37.91 1.41 17.19 3.06 34.29 7.42 50.15 14.89 8.16 3.88 16.42 7.83 23.64 13.32l13.13 11.88c13.45 12.86 23.85 28.41 34.19 43.76 4.47 7.53 7.32 15.92 9.7 24.31-4.47-4.56-8.48-9.53-13.06-13.97 4.48 10.44 11.36 19.6 16.26 29.82 2.29 4.93 4.95 9.67 7.57 14.42 2.49 4.71 3.39 10.02 4.95 15.07 2.22 7.81 5.28 15.46 6.44 23.53 0.48 21.42 3.21 42.95 0.14 64.3-1.84 12.13-4.52 24.5-10.99 35.09l-5.24 8.8-0.87 1.15c-7.2 6.99-16.68 11.55-26.49 13.48 0.98-7.39 5.64-13.49 7.29-20.67-4.94 7.17-9.18 15.37-17.04 19.82a154.33 154.33 0 0 1-15.74 5.36c-9.48 2.56-19.22-0.45-28.2-3.47 7.61-6.72 15.24-14.07 19.01-23.73 2.23-5.74 2.5-11.98 2.71-18.07l-6.1 8.14c-0.91 0-1.81-0.01-2.72 0.04l-0.27 0.24c-5.49 4.59-9.83 10.61-16.26 13.99-6.77 3.9-14.52 5.84-22.32 5.86 0.08-7.5 5.51-13.02 9.69-18.66 3.11-2.51 6.01-5.34 7.92-8.89 8.78-13.48 12.73-30.46 9.5-46.31 1.96-21.08 2.04-42.44-1.24-63.4-1.09-8.14-2.93-16.16-3.83-24.32 2.69 1.25 5.32 2.84 6.96 5.4l10.47 15.05c3.26-1.11 6.46-2.39 9.58-3.85-2.5-4.43-4.89-8.92-7.13-13.47 5.13 0.41 10.2 1.43 15.34 1.75 3.29 0.27 4.99-3.22 6.97-5.26 3.53-4.94 8.34-8.94 11.28-14.27l-0.24-1.25c-4.33-1.49-9.85-3.63-13.9-0.38-1.9 1.05-3.77 2.83-6.11 2.37-6.19-0.85-12.22-2.61-18.4-3.5 3.3-2.75 5.96-6.12 8.54-9.53-3.59-0.84-7.22-2.61-10.97-1.97-4.83 1.91-7.95 6.63-12.79 8.5-4.5 0.46-8.91-1.33-13.42-0.89-6.72 0.84-11.48 9.44-7.86 15.36 4.12 5.69 11.77 4.64 17.78 6.21 0.24 0.25 0.71 0.77 0.95 1.03-0.7 9.97 0.83 19.88 1.27 29.82 0.43 7.85 2.09 15.69 1 23.57-11.34-5.15-20.3-14.24-27.6-24.13-3.13-4.46-4.94-9.64-7.16-14.58-1.37 12.33 1.12 24.68 5.19 36.28-6.47-2.05-13.09-4.05-18.76-7.91-14.12-9.24-23.85-24.04-29.14-39.86-2.56-7.26-2.15-15.04-2.59-22.61-5.18 7.82-5.47 17.52-6.63 26.52-1.61 13.16-1.26 26.8 3.32 39.38-17.42 0.34-35.17-6.44-46.83-19.61-4.72-5.08-7.06-11.72-9.45-18.09-1.52 9.4-3.43 19-1.8 28.53-5.45-1.48-10.9-3.01-16.16-5.08-6.8-6.84-10.55-16.09-12.6-25.37-2.3-10.11-2.38-20.55-1.9-30.84 0.38-8.75 2.54-17.27 3.46-25.96-1.59 4.49-2.7 9.11-4.07 13.66-1.32 4.78-5.45 7.85-8.59 11.41-14.7 16.71-26.78 36.02-33.44 57.36-3.15-5.95-5.25-12.45-5.91-19.16-0.37-5.33 0.46-10.64 1.73-15.81 1.8-7.5 3.31-15.08 5.43-22.5l0.96-1.99c7.22-9.51 13.03-20.11 21.41-28.71 4.34-3.92 10.42-4.75 15.45-7.47 4-2.18 8.6-4.17 11.08-8.19 1.64-4.08-2.08-7.53-3.99-10.76l-2.84 1.03c-5.99 1.99-11.21 5.77-15.34 10.5-4.32 4.15-7.05 9.55-9.95 14.71-7.67 8.25-13.28 18.12-18.65 27.95-11.6 22.74-18.11 47.72-22.1 72.83-6.21 26.02-6.14 53.79 2.07 79.38-7.84-0.1-15.61-3.46-20.97-9.2-4.39-4.67-8.32-9.78-11.59-15.3-1.35-2.53-3.05-4.83-4.88-7.02-1.22 6.35-2.17 12.84-1.72 19.33 0.34 4.73 1.3 9.39 2.03 14.08-4.74-1.34-9.6-2.6-13.81-5.26-0.93-0.74-1.79-1.55-2.72-2.27-3.25-2.38-6.9-4.3-9.56-7.39-5.13-5.73-8.48-12.75-13.52-18.55-1.84-3.25-1.44-7.23-1.94-10.82-0.21-6.9-1.31-13.98 0.48-20.75 3.01-8.49 2.32-17.68 4.9-26.26 4.49-15.19 11.98-29.31 20.39-42.67 6.71-10.93 14.39-21.24 22.43-31.23 0.66 3.47-0.02 8.05 3.29 10.32 3.24 2.46 8.42 3.41 11.44 0.08 3.84-4.14 7.19-9.42 7.36-15.21 0.05-3.75-3.55-5.77-6.08-7.86 3.89-7.8 6.8-16.38 13.06-22.7 3.99-4.05 8.06-8.08 11-13 1.34-1.96 3.15-3.52 5.01-4.95 21.41-16.61 46.53-28.65 73.16-33.92 3.96-0.72 7.9-1.86 11.96-1.8 4.38 1.96 8.74 4.1 13.51 4.97 10.12 1.98 20.6 4.67 28.85 11.14 1.38 1.28 2.55 2.75 3.67 4.25 1.06-6.88 0.81-13.87 0.5-20.8m45.55 57.63c-1.99 0.64-5.04 1.4-4.94 4-0.47 3.3 2.64 5.46 5.22 6.75 6.05 2.95 12.23 5.7 17.99 9.21 4.91 3.01 8.12 7.91 11.53 12.39 1.46-2.22 1.75-4.94-0.57-6.7-3.97-10.21-10.84-19.87-20.7-25.05-2.62-1.58-5.7-1.34-8.53-0.6m-193.33 33.4c-3.69 1.74-6.11 7.49-2.38 10.44 4.37 3.5 10.55-2.09 10.14-6.88 0.51-4.11-4.9-4.88-7.76-3.56m81.96 16.75c-4.48 2.48-3.72 12.53 2.4 11.71 3.85-1.04 4.53-6.09 3.69-9.43-0.61-2.61-3.92-3.49-6.09-2.28m11.01 4.06c-2.01 2.09-2.32 6.34 0.57 7.8 2.84 1.73 6.52-1.85 5.78-4.85-0.37-2.9-3.89-4.3-6.35-2.95m95.88 6.8c-3.11 2.89 0.35 8.48 4.36 6.81 3.8-2.48-0.35-8.9-4.36-6.81m104.6 1.85c0.23 2.44-1.1 6.42 2.31 7.11 3.32 1.21 6.12-3.46 3.65-5.9-1.27-2.24-3.92-1.59-5.96-1.21m-96.19-0.27c0.28 1.55-0.11 3.36 0.8 4.73a35.48 35.48 0 0 0 3.69 1.07 48.89 48.89 0 0 0-0.38-4.2c-0.94-1.24-2.79-1.03-4.11-1.6m108.45 1.75c-1.23 0.39-2.94 1.22-2.96 2.7-0.69 2.8 3.92 5.24 5.59 2.69 2.17-1.78-0.1-5.6-2.63-5.39zM89.63 337.62c4.84-3.42 10.45-5.43 16.14-6.89-0.83 3.63-1.54 7.28-2.17 10.95-3.57 0.83-7.31 1.47-10.43 3.51-2.68 1.75-4.95 4.03-7.38 6.09 0.72-4.64 1.1-9.7 3.84-13.66zm469.64 10.36l3.87-0.26c9.68 2.07 18.53 7.98 23.96 16.32-4.01-1.28-7.95-2.73-11.78-4.45-4.98-2.56-10.63-2.97-16.12-2.87l0.07-8.74zm-466.2 18.09c4.2-4.82 8.86-9.76 14.98-12.03l1.55-0.62c-1.75 3.83-3.74 7.68-4.31 11.91-0.69 5.3-0.03 10.65 0.65 15.92-3.33 0.2-7.64-0.29-9.84 2.81-2.74 4.62-3.23 10.11-4.72 15.18-4.07-6.25-5.08-13.98-4.67-21.29 0.09-4.78 3.6-8.35 6.36-11.88zm455.76 9.83c10.94-0.93 23.11 0.98 31.09 9.17 5.94 5.57 7.21 14.21 7.41 21.96 0.02 8.37-4.47 15.91-10.31 21.6l-1.79-5.74c-1.06-3.08-1.98-6.38-4.29-8.78-3.21-3.53-7.6-5.73-10.77-9.27-1.11-4.27-1.73-8.67-3.18-12.84-1.91-5.75-5.04-10.96-8.16-16.1zm-33.05 91.88c8.75-8.98 15.43-19.65 21.04-30.81 1.55 10.95 1.16 22.04 0.48 33.04-0.44 6.71-0.46 13.44-1.27 20.12-1.82 10.95-5.12 21.72-5.51 32.89-0.27 6.48 1.55 12.79 1.94 19.23 0.79 14.51 4.23 28.87 10.02 42.2 3.86 8.93 8.5 17.5 12.91 26.17 3.89 7.56 7.52 15.69 7.11 24.39 0.06 10.73-5.92 20.51-13.77 27.43l-3.55-16.72c-2.65-13.26-8.01-25.98-16.03-36.89-1.5-2.1-3.32-4-4.38-6.36-0.83-3.76-0.52-7.67-0.98-11.47-0.39-5.23-1.98-10.25-2.99-15.37-2.56-12.59 1.75-25.86-3.39-38.01-3.71 6.63-6.27 14.73-3.17 22.12 2.81 6.55 4.35 13.5 5.44 20.52 1.97 10.64 3 22.05-1.04 32.35-3 8.02-9.33 14.37-16.54 18.77 1.74-6.57 3.01-13.41 1.68-20.18-2.63-14.84-6.92-29.55-14.11-42.87-6.97-14.05-16.47-27.37-18.97-43.17 0.35-10.21 1.13-20.45 2.55-30.58 16.59-4.24 30.63-14.87 42.53-26.8zm-381.61 33.81c3.28 0.46 6.38-0.56 8.94-2.6 7.48 3.18 14.87 6.58 22.45 9.52 7.29 3.22 15.04 5.36 22 9.29-1.72 4.58-3.22 9.23-4.5 13.95l7.88 3.43c-4.48 18.79-12.13 36.56-19.37 54.4-6.23 15.05-8.89 31.58-7.49 47.82-7.93-6.26-13.77-15.33-15.02-25.47-1.89-14.08-0.04-28.59 4.08-42.13 1.84-6.21 3.84-12.43 4.8-18.85 0.5-4.03 0.25-9.05-3.34-11.65-3.45-1.26-4.87 3.33-5.28 5.94-0.58 7.87 2.79 15.5 1.95 23.42-0.67 4.21-2.33 8.18-3.27 12.33-1.74 7.55-2.86 15.28-2.64 23.05 0.45 5.03-3.57 8.63-5.51 12.88-3.41 5.99-4.92 12.8-8.09 18.88-4.92 7.39-7.77 15.82-11.61 23.75-8.77-9.08-12.49-22.14-11.9-34.56-0.24-7.92 2.33-15.5 4.88-22.87 2.86-8.09 7.19-15.54 10.67-23.37 2.34-4.83 3.87-10.01 6.4-14.75l-0.17 2.06c6.39-14.1 8.49-30 6.39-45.31-0.48 4.17-0.87 8.35-1.22 12.54 0.32-10.59-0.21-21.15-1.03-31.7zm163.25 134.83c3.01-1.29 7.63-1.5 9.36 1.88 3.03 4.69-1.72 12.49-7.43 10.49-5.66-1.08-8.03-10.13-1.93-12.37m0.71 1.52c-1.49 1.61-1.22 3.94-1.47 5.96l3.48 0.04c0.41-2.32 1.77-6.13-2.01-6z\" />\n\n        <path\n            android:fillColor=\"#fffa7db2\"\n            android:pathData=\"M148.5 109.86l5.39-0.21c1.61 1.26 1.55 3.48 2.05 5.29 4.21 20.39 8.37 41.1 7.39 62.03-0.01 3.85-6.18 4.8-7.43 1.2-3.21-9.18-3.97-19-5.71-28.52-1.66-13.16-4.29-26.57-1.69-39.79zM92.07 122.1c5.41-4.87 12.36-9.18 19.96-8 5.21 0.05 8.66 4.75 10.81 8.94 2.68 7.66 2.36 15.95 2.41 23.96 0.33 15.75-4.79 30.98-10.99 45.25-1.63 4.13-2.77 8.46-4.64 12.5-0.98 2.26-3.29 3.73-5.72 3.88-7.1 0.5-13.95-1.85-20.63-3.93-6.63-2.04-12.55-5.79-19.09-8.07-6.07-2.13-11.93-4.86-17.2-8.57-5.65-3.94-11.7-7.67-15.9-13.24-3.92-5.09-2.93-11.9-2.6-17.88 0.45-7.99 4.51-15.77 11.06-20.44 3-2.21 6.87-2.4 10.44-2.67 4.79-0.18 9.68-0.23 14.35 0.97 5.38 1.6 9.57 5.56 13.79 9.07 2.77-8.31 8.1-15.39 13.95-21.77zM9.67 234.35c-3.96-1.91-1.96-9.25 2.43-8.98 6.75 0.12 13.18 2.56 19.79 3.72 13.72 3.47 27.5 7.35 40.15 13.85-0.95 2.26-1.85 4.54-2.73 6.83-6.88-1.15-13.77-2.34-20.47-4.32-9.46-2.72-19.19-4.48-28.46-7.84-3.51-1.27-7.27-1.77-10.71-3.26z\" />\n\n        <path\n            android:fillColor=\"#ffd26c91\"\n            android:pathData=\"M293.71 135.87c7-1.5 14.18-1.47 21.3-1.92 2.86 0.03 6.26-0.61 8.53 1.58 5.14 4.61 10.61 9.56 12.53 16.45-8.25-6.47-18.73-9.16-28.85-11.14-4.77-0.87-9.13-3.01-13.51-4.97zm158.23 29.18c8.16 4.03 15.28 9.92 22.1 15.89 23.56 20.58 41.4 47.41 52.44 76.61 4.28 11.37 8.76 22.87 10.05 35.04 1.94 11.74 3.06 23.66 2.11 35.56-4.66-21.04-9.88-42.35-20.5-61.3l1.59 8.42c-2.62-4.75-5.28-9.49-7.57-14.42-4.9-10.22-11.78-19.38-16.26-29.82 4.58 4.44 8.59 9.41 13.06 13.97-2.38-8.39-5.23-16.78-9.7-24.31-10.34-15.35-20.74-30.9-34.19-43.76l-13.13-11.88zm-193.98 17.8l2.84-1.03c1.91 3.23 5.63 6.68 3.99 10.76-2.48 4.02-7.08 6.01-11.08 8.19-5.03 2.72-11.11 3.55-15.45 7.47-8.38 8.6-14.19 19.2-21.41 28.71 3.38-10.63 10.88-19.07 15.82-28.89 2.9-5.16 5.63-10.56 9.95-14.71 6.13-3.59 11.83-8.16 18.97-9.62l-3.63-0.88zm122.89 14.21c-0.1-2.6 2.95-3.36 4.94-4 9.69 1.63 17.27 8.68 22.65 16.5 1.99 3.19 4.05 6.35 6.58 9.15 2.32 1.76 2.03 4.48 0.57 6.7-3.41-4.48-6.62-9.38-11.53-12.39-5.76-3.51-11.94-6.26-17.99-9.21-2.58-1.29-5.69-3.45-5.22-6.75zm46.27 66.86c3.08 5.86 3.43 12.6 3.88 19.06 0.78 12.02 2.8 24 2.3 36.08l-3.91-1.75c1.09-7.88-0.57-15.72-1-23.57-0.44-9.94-1.97-19.85-1.27-29.82zm-155.34 32.23c5.26 2.07 10.71 3.6 16.16 5.08 0.47 2.67 1.1 5.31 1.7 7.95-6.67-3.24-12.97-7.38-17.86-13.03zm-77.79 92.07c-8.21-25.59-8.28-53.36-2.07-79.38-1.76 18.31-2.94 37 1.13 55.1 1.84 9.12 5.85 18.26 13.73 23.72-4.26-0.99-8.58-0.31-12.79 0.56zm348.05-35.21c5.65-4.97 13.81-6.2 21.1-5.29l-3.87 0.26-0.07 8.74c5.49-0.1 11.14 0.31 16.12 2.87-6 0.16-12.13-0.01-17.93 1.76-6.3 1.69-12.1 4.79-17.47 8.43 0.61-5.58 0.36-11.38 2.12-16.77zm-433.99 1.03l3.06-1.63c1.87 7.7 3.23 15.51 5.6 23.08-3.38 2.27-6.74 4.74-10.77 5.76-0.68-5.27-1.34-10.62-0.65-15.92 0.57-4.23 2.56-8.08 4.31-11.91l-1.55 0.62zm9.2 2.6c5.04 5.8 8.39 12.82 13.52 18.55 2.66 3.09 6.31 5.01 9.56 7.39 0.12 4.44-0.18 8.89 0.18 13.32 0.46 5.28 2.65 10.16 4.34 15.11-3.13-2.07-6.22-4.31-8.58-7.27-5.51-6.85-10.89-14.07-13.58-22.56-2.59-7.96-5.61-16.05-5.44-24.54zm39.3 0.06c1.83 2.19 3.53 4.49 4.88 7.02 0.92 4.16 2.66 8.13 5.47 11.37 3.25 4.54 8.3 8.41 8.74 14.38 0.67 6.24 0.54 12.53 0.59 18.8-2.52 0.04-5.04 0.14-7.56 0.27-3-3.33-7.45-4.62-11.03-7.16-5.95-4.32-11.95-9.45-14.59-16.53 4.21 2.66 9.07 3.92 13.81 5.26-0.73-4.69-1.69-9.35-2.03-14.08-0.45-6.49 0.5-12.98 1.72-19.33zm382.69 20.68l9.59-1.48c3.12 5.14 6.25 10.35 8.16 16.1 1.45 4.17 2.07 8.57 3.18 12.84-3.8-1.82-7.34-4.13-11.11-6.01-4.46-2.05-9.25-3.25-13.87-4.87l4.05-16.58zm-395.13 14.59c3.05 3.57 5.97 7.32 9.7 10.23 3.16 2.46 6.64 4.49 10.01 6.65-2.17 3.28-5.44 7.36-3.15 11.42 2.39 2.76 6.19 3.6 9.52 4.69-2.49 8.71-2.15 18.42 2.38 26.42 3.12 6.68 8.83 11.45 13.68 16.8-5.09-1.72-9.77-4.39-14.42-7.03-3.44-1.98-7.22-3.36-10.49-5.61-0.56-1.88 0.12-3.85 0.27-5.73-8.82-5.73-20.22-4.6-28.95-10.4-2.76-1.43-1.85-4.88-1.91-7.4 0.75-8.46 1.3-17.09 4.25-25.14 4.99 3.86 9.97 8.24 16.41 9.33-1.58-3.52-3.6-6.8-5.37-10.21-2.18-4.34-2.05-9.3-1.93-14.02zm305.49 12.1l6.1-8.14c-0.21 6.09-0.48 12.33-2.71 18.07-3.77 9.66-11.4 17.01-19.01 23.73 8.98 3.02 18.72 6.03 28.2 3.47a154.33 154.33 0 0 0 15.74-5.36c-4.08 4.64-9.75 7.35-15.33 9.71-8.1 3.27-16.81 5.18-25.56 5.19-6.13-0.04-12.41-0.16-18.21-2.36 3.22-1.15 6.49-2.34 9.33-4.28 2.52-1.68 2.18-5.11 2.85-7.73 3.06-4.62 5.89-9.46 7.43-14.83 1.78-6.13 4.81-11.79 8.18-17.19l0.27-0.24c0.91-0.05 1.81-0.04 2.72-0.04zm83.92-4.99c2.47 1.58 5.21 3.13 6.58 5.87 7.1 13.27 7.27 28.91 5.43 43.5-2.15 14.09-5.62 27.97-9.5 41.68 0.81-6.68 0.83-13.41 1.27-20.12 0.68-11 1.07-22.09-0.48-33.04-5.61 11.16-12.29 21.83-21.04 30.81-11.9 11.93-25.94 22.56-42.53 26.8-1.42 10.13-2.2 20.37-2.55 30.58-5.17-16.95-4.9-35.08-2.12-52.43 1.23-7.39 2.44-14.88 5.21-21.88 1.25-4.04 6.25-3.66 9.58-4.61 15.04-2.23 28.35-11.47 37.36-23.5 5.54-7.12 9.32-15.38 12.79-23.66zm33.57 16.46c1.51 1.61 3.3 3.06 4.31 5.08 0.94 2.81 1.02 5.81 1.41 8.73 1.7-1.74 2.17-4.14 2.42-6.46l1.79 5.74c-5.24 5.08-11.42 9.93-18.97 10.54 2.96-3.09 6.64-5.94 7.83-10.26 1.19-4.35 0.9-8.92 1.21-13.37zm-79.42 21.15c9.81-1.93 19.29-6.49 26.49-13.48-7.49 10.44-19.29 17.01-31.21 21.17 1.24-2.57 2.6-5.08 4.04-7.55l0.68-0.14zM149.32 545.24c0.41-2.61 1.83-7.2 5.28-5.94 3.59 2.6 3.84 7.62 3.34 11.65-0.96 6.42-2.96 12.64-4.8 18.85l-1.87-1.14c0.84-7.92-2.53-15.55-1.95-23.42zm-29.17 114.31c3.84-7.93 6.69-16.36 11.61-23.75-1.44 10.72-4.86 22.22 0.06 32.56-4.58-1.92-8.45-5.06-11.67-8.81z\" />\n\n        <path\n            android:fillColor=\"#ff75202d\"\n            android:pathData=\"M242.62 193.35c4.13-4.73 9.35-8.51 15.34-10.5l3.63 0.88c-7.14 1.46-12.84 6.03-18.97 9.62zm143.17-0.29c2.83-0.74 5.91-0.98 8.53 0.6 9.86 5.18 16.73 14.84 20.7 25.05-2.53-2.8-4.59-5.96-6.58-9.15-5.38-7.82-12.96-14.87-22.65-16.5zm-153.12 15c-4.94 9.82-12.44 18.26-15.82 28.89l-0.96 1.99c-3.1 5.58-6.48 11.27-7.22 17.75-0.77 6.83-0.51 13.73 0.06 20.56 0.66 6.71 2.76 13.21 5.91 19.16 6.66-21.34 18.74-40.65 33.44-57.36 3.14-3.56 7.27-6.63 8.59-11.41 1.37-4.55 2.48-9.17 4.07-13.66-0.92 8.69-3.08 17.21-3.46 25.96-0.48 10.29-0.4 20.73 1.9 30.84 2.05 9.28 5.8 18.53 12.6 25.37 4.89 5.65 11.19 9.79 17.86 13.03-0.6-2.64-1.23-5.28-1.7-7.95-1.63-9.53 0.28-19.13 1.8-28.53 2.39 6.37 4.73 13.01 9.45 18.09 11.66 13.17 29.41 19.95 46.83 19.61-4.58-12.58-4.93-26.22-3.32-39.38 1.16-9 1.45-18.7 6.63-26.52 0.44 7.57 0.03 15.35 2.59 22.61 5.29 15.82 15.02 30.62 29.14 39.86 5.67 3.86 12.29 5.86 18.76 7.91-4.07-11.6-6.56-23.95-5.19-36.28 2.22 4.94 4.03 10.12 7.16 14.58 7.3 9.89 16.26 18.98 27.6 24.13l3.91 1.75c0.5-12.08-1.52-24.06-2.3-36.08-0.45-6.46-0.8-13.2-3.88-19.06l-0.95-1.03c-6.01-1.57-13.66-0.52-17.78-6.21-3.62-5.92 1.14-14.52 7.86-15.36 4.51-0.44 8.92 1.35 13.42 0.89 4.84-1.87 7.96-6.59 12.79-8.5 3.75-0.64 7.38 1.13 10.97 1.97-2.58 3.41-5.24 6.78-8.54 9.53 6.18 0.89 12.21 2.65 18.4 3.5 2.34 0.46 4.21-1.32 6.11-2.37 4.05-3.25 9.57-1.11 13.9 0.38l0.24 1.25c-2.94 5.33-7.75 9.33-11.28 14.27-1.98 2.04-3.68 5.53-6.97 5.26-5.14-0.32-10.21-1.34-15.34-1.75 2.24 4.55 4.63 9.04 7.13 13.47-3.12 1.46-6.32 2.74-9.58 3.85l-10.47-15.05c-1.64-2.56-4.27-4.15-6.96-5.4 0.9 8.16 2.74 16.18 3.83 24.32 3.28 20.96 3.2 42.32 1.24 63.4-1.7 10.13-3.59 20.26-6.78 30.04-2.64 8.74-6.6 17-10.64 25.16-4.18 5.64-9.61 11.16-9.69 18.66 7.8-0.02 15.55-1.96 22.32-5.86 6.43-3.38 10.77-9.4 16.26-13.99-3.37 5.4-6.4 11.06-8.18 17.19-1.54 5.37-4.37 10.21-7.43 14.83l-2.33-1.81 1.91-3.11c2.03-3.29 3.76-6.75 5.42-10.23-9.7 4.14-20.21 5.66-30.69 6.04-1.2 1.56-2.4 3.13-3.69 4.62-2.98 1.66-5.88 3.45-8.73 5.32l-49.67-31.45c-4.05-2.61-8.48-4.67-12.22-7.74-1.99-1.47-2.12-4.29-1.51-6.48 1.71-2.43 4.53-3.7 7.09-5.04 18.83-8.91 37.24-18.66 55.78-28.14 6.54-3.19 12.96-7.34 20.34-8.12 1.62 1.34 2.05 3.49 2.46 5.45 1.34 7.65 2.54 15.36 2.72 23.15-4.25 1.88-8.57 3.62-13 5.02-7.16 2.27-13.99 5.48-21.21 7.55-4.92 1.42-9.65 3.42-14.29 5.56 11.71 6.68 23.46 13.34 35.5 19.41 17.39-20.65 24.52-47.82 27.42-74.14 0.58-6.73 0.87-13.49 1.11-20.23-9.94-3.07-18.8-8.96-25.73-16.68-3.9-4.15-7.13-8.84-10.53-13.38 0.89 7.8 2.85 15.47 6.54 22.44l0.13 3.06c-7.09-0.34-14.07-1.86-20.66-4.5-7.11-4.73-14.03-10-19.3-16.8-6.09-7.77-11.31-16.44-13.95-26.01l-3.2-11.4c-1.31 11.22-1.86 22.57-1.07 33.85 0.46 6.87 2.58 13.49 5.5 19.7a65.74 65.74 0 0 1-4.05 0.94c-23.06 2.94-47.18-10.13-56.96-31.28-0.71 10.33-0.49 21.17 4.57 30.5l-0.15 1.77c-8.46-1.48-15.94-5.9-22.69-11.02-6.12-6.24-11.45-13.43-14.25-21.8-5.2-13.55-4.27-28.31-3.44-42.51-4.24 3.5-8.17 7.42-11.3 11.96-8.29 11.87-16 24.29-21.16 37.87-1.81 4.74-2.68 9.76-4.15 14.61l-1.24 0.72c-1.96-2.34-3.76-4.81-5.32-7.42-3.66-7-5.58-14.82-5.63-22.72-3.06 8.3-6.72 16.46-8.31 25.22-2.59 14.77-3.57 29.81-3 44.8l1.41-1.52c3.84-3.73 6.83-8.52 11.58-11.17 3.95 1.67 6.54 5.28 9.55 8.17l38.94 39.05c2.76 2.75 6.52 6.75 3.75 10.79-3.58 3.17-8.39 4.37-12.63 6.41l-38.06 16.9c-3.31 1.45-6.67 2.88-9.66 4.94-1.99-2.27-3.97-4.55-5.58-7.1-0.92-4.47-1.44-9.03-1.34-13.59 0.4-1.58 0.9-3.12 1.42-4.65-4.26-0.36-8.5-0.92-12.7-1.72a296.79 296.79 0 0 0 0.18 17.9 91.69 91.69 0 0 0-4.69-0.06c-0.05-6.27 0.08-12.56-0.59-18.8-0.44-5.97-5.49-9.84-8.74-14.38-2.81-3.24-4.55-7.21-5.47-11.37 3.27 5.52 7.2 10.63 11.59 15.3 5.36 5.74 13.13 9.1 20.97 9.2 4.21-0.87 8.53-1.55 12.79-0.56-7.88-5.46-11.89-14.6-13.73-23.72-4.07-18.1-2.89-36.79-1.13-55.1 3.99-25.11 10.5-50.09 22.1-72.83 5.37-9.83 10.98-19.7 18.65-27.95m210.58 28.6c-4.62 2.32-7.86 6.74-12.59 8.86-5.81 0.64-12.33-3.44-17.54 0.6-4.55 2.44-3.53 9.94 1.27 11.43 6.84 2.07 14.12 1.75 21.07 3.24 1.17 0.74 1.8 2.04 2.59 3.15 3.39 5.53 7.18 10.8 11.1 15.96l3.74-1.8c-2.79-5-5.57-10.05-7.21-15.56 6.85-0.06 13.6 1.26 20.38 2.01 4.57-5.06 8.88-10.34 13.12-15.67-2.22-0.46-4.47-1.61-6.75-1.25-2.11 1.25-3.77 3.11-5.54 4.77-9.34-1.9-18.83-2.94-28.11-5.13 3.04-3.45 6.33-6.68 9.33-10.16-1.62-0.2-3.23-0.56-4.86-0.45M195.06 354.87c1.17 9.86 3.59 20.17 10.08 27.97 1.69 1.6 3.65 4.35 6.31 3.46 5.49-1.68 10.82-3.85 16.09-6.12-5.15-4.1-10.7-7.69-15.66-12.03-5.4-4.68-10.85-9.33-16.82-13.28zM415.8 253.03c0.32-2.92 2.42-4.77 5.28-5.11-0.1 3.08-2.61 4.46-5.28 5.11z\" />\n\n        <path\n            android:fillColor=\"#fffeecdb\"\n            android:pathData=\"M192.46 226.46c2.86-1.32 8.27-0.55 7.76 3.56 0.41 4.79-5.77 10.38-10.14 6.88-3.73-2.95-1.31-8.7 2.38-10.44z\" />\n\n        <path\n            android:fillColor=\"#fffefdfe\"\n            android:pathData=\"M443.25 236.66c1.63-0.11 3.24 0.25 4.86 0.45-3 3.48-6.29 6.71-9.33 10.16 9.28 2.19 18.77 3.23 28.11 5.13 1.77-1.66 3.43-3.52 5.54-4.77 2.28-0.36 4.53 0.79 6.75 1.25-4.24 5.33-8.55 10.61-13.12 15.67-6.78-0.75-13.53-2.07-20.38-2.01 1.64 5.51 4.42 10.56 7.21 15.56l-3.74 1.8c-3.92-5.16-7.71-10.43-11.1-15.96-0.79-1.11-1.42-2.41-2.59-3.15-6.95-1.49-14.23-1.17-21.07-3.24-4.8-1.49-5.82-8.99-1.27-11.43 5.21-4.04 11.73 0.04 17.54-0.6 4.73-2.12 7.97-6.54 12.59-8.86m-27.45 16.37c2.67-0.65 5.18-2.03 5.28-5.11-2.86 0.34-4.96 2.19-5.28 5.11m6.99 1.17c2.71-0.63 4.89-2.34 5.22-5.23-2.86 0.41-4.54 2.55-5.22 5.23m6.74-0.17c2.62 2.75 4.67-1.88 4.78-4.14-1.81 1.11-3.64 2.3-4.78 4.14zM98.97 423.34c4-0.14 7.99 0.2 11.95 0.78l1.76 2.74 1.36-1.57 9.07 5.22c0.96 3.71 1.73 7.93 5.02 10.36 3.73 2.7 8.25 4.04 12.61 5.38 5.48 1.4 10.85 3.22 16.36 4.51-1 4.39-2.39 8.73-4.82 12.56-6.61 11.1-11.77 23.18-19.85 33.34-9.07-4.92-17.91-10.28-27.15-14.89-6-3.31-12.8-0.2-19.01 0.77-2.75-5.78-5.23-11.75-6.26-18.11 2.98-2.18 6.65-5.01 5.73-9.24-0.06-4.34-4.18-6.4-7.74-7.59-0.76-5.59-0.85-11.24-0.37-16.86l3.3-0.2-0.89-4.13c5.24-1.03 10.58-1.75 15.93-1.53 1.42 0.47 2.72 1.22 4.07 1.86l-1.07-3.4zm344.6 52.36c5.22-1.2 10.63-0.94 15.74 0.63 2.26 4.46 5.35 9.29 3.73 14.51-1.29 2-3.93 2.6-5.9 3.78-4.32-6.45-8.6-12.94-13.57-18.92zm-9.99 10.31l5.44-6.34c4.44 5.56 8.52 11.38 12.36 17.37l-8.21 3.07c-3.11-4.76-6.39-9.41-9.59-14.1zm-216.79-1.9c14.14 6.25 29.19 9.92 44.09 13.85 3.85 3.37 7.64 6.85 10.71 10.97-2.61 9.96-4.46 20.18-4.36 30.51l5.67 0.47c-1.09 9.26-1.56 18.57-2.07 27.87l-0.94 12.8-10.21-0.66c0.03-3.77-0.03-7.54-0.19-11.29l-2.66-1.75-2.83-1.93a123.77 123.77 0 0 0 3.77 1.53c-1.11-6.28-3.34-12.34-6.68-17.79-2.54-4.66-5.55-9.06-9.15-12.97a75.83 75.83 0 0 0 2.45 11.14c-7.7 3.16-15.51 6.2-23.65 7.98-3.39 0.86-6.82-0.24-9.85-1.72-3.63-4.96-6.98-10.11-10.43-15.2l3.27-4.96c2.61-2.55 5.24-5.19 6.8-8.54 2.67-5.49 5.65-10.96 6.93-16.98 1.05-5.22-0.38-10.46-0.95-15.64l0.28-7.69zm202.72 19.79l5.56 7.35c-2.38 3-4.79 5.99-7.29 8.89-1.9-2-3.71-4.08-5.41-6.24 2.04-3.56 4.4-6.94 7.14-10zm-56 4.6c2.66-1.2 5.62-2.58 8.59-1.65 4.65 1.28 8.87 3.72 13.15 5.89 3.55 1.91 7.52 3.88 9.29 7.75 2.72 4.87 3.76 11.04 8.32 14.67 4.77 2.24 10.24 1.35 15.34 1.74 4.04 1.45 7.25 4.83 9.45 8.43-0.03 7.46-2.61 15.85-9.25 20.06-3.55 2.28-6.86 5.25-10.97 6.43-5.7 0.15-12.04-0.99-17.02 2.48-3.85 2.42-5.78 6.68-7.8 10.56-1.94 2.56-5.61 4.14-8.8 3.25-3.75-0.89-7.61-1.45-11.23-2.81-2.91-5.61-3.22-12.13-3.14-18.32 0.12-2.47-0.45-4.89-1.44-7.12-3.07 7.37-0.57 15.42-0.2 23.06-3.46-4.47-9.59-2.61-14.4-2.46-1.33-11.19-0.3-22.58-2.43-33.67l-0.84-3.38 0.02-1.49 5.75-0.44 1.46-0.11c1.28-9.05 1.11-18.3 3.3-27.2 4.4-1.63 8.63-3.65 12.85-5.67zM297.7 629.7l7.4-6.08 1.09 10.79c-3.46-0.32-6.95-0.32-10.41 0.01l-0.57-8.58 2.49 3.86zm10.71 6.92c3.62 2.73 6.82 5.98 9.68 9.49 2.63 4.76 4.14 10.3 7.96 14.29 10.34-0.56 20.01-4.81 29.93-7.45 2.2 5.56 3.75 11.35 5.54 17.05h-49.91c-2.32-11-1.75-22.3-3.2-33.38zm-29.58 26c4.35-6.59 8-13.62 12.59-20.05 2.11 3.63 4.78 7.1 5.62 11.31 1.11 5.31 1.47 10.75 2.24 16.12h-29.56l0.26-11.07c2.91 1.32 5.85 2.58 8.85 3.69zm180.15-5.77c7.3-5.1 14.74-10.15 22.94-13.71 5.43 9.07 10.91 18.12 16.87 26.86h-49.33c3.63-4.04 6.4-8.73 9.52-13.15zm-160.68-5.28c3.3-0.81 6.54-1.83 9.69-3.1 0.41 7.18 1.12 14.35 1.57 21.53h-8.22c-1.26-6.1-1.89-12.31-3.04-18.43z\" />\n\n        <path\n            android:fillColor=\"#ffd46e93\"\n            android:pathData=\"M208.67 256.69c0.74-6.48 4.12-12.17 7.22-17.75-2.12 7.42-3.63 15-5.43 22.5-1.27 5.17-2.1 10.48-1.73 15.81-0.57-6.83-0.83-13.73-0.06-20.56zm322.45 57.18c4.7 18.59 6.03 38.07 4.1 57.14-1.28 11.71-4.26 23.28-9.16 34-3.06 6.03-5.98 12.38-11.03 17.05 1.81-2.9 3.49-5.87 5.24-8.8 6.47-10.59 9.15-22.96 10.99-35.09 3.07-21.35 0.34-42.88-0.14-64.3zm14.06 331.85c1.34 5.54 2.38 11.14 3.55 16.72-2.72 1.67-5.49 3.23-8.3 4.73 4.75-6.15 4.48-14.1 4.75-21.45z\" />\n\n        <path\n            android:fillColor=\"#ffffae94\"\n            android:pathData=\"M242.87 250.84c3.13-4.54 7.06-8.46 11.3-11.96-0.83 14.2-1.76 28.96 3.44 42.51 2.8 8.37 8.13 15.56 14.25 21.8-3.59-2.34-7.34-4.66-9.94-8.16-5.93-7.36-8.8-16.48-11.77-25.3-7.7 7.76-16.49 14.74-22.14 24.27-1.42 2.31-2.71 4.71-4.01 7.09-0.99 0.12-1.97 0.28-2.95 0.49-1.2 0.51-2.36 1.1-3.49 1.74 1.47-4.85 2.34-9.87 4.15-14.61 5.16-13.58 12.87-26 21.16-37.87zm103.84 7.89c1.13 3.78 2.12 7.6 3.2 11.4 2.64 9.57 7.86 18.24 13.95 26.01 5.27 6.8 12.19 12.07 19.3 16.8-11.13-2.83-19.34-11.09-27.91-18.17-0.28 5.33 0.78 10.76-0.3 16.02-1.22 0.63-2.52 1.07-3.81 1.49-2.92-6.21-5.04-12.83-5.5-19.7-0.79-11.28-0.24-22.63 1.07-33.85zm-149.65 40.39c1.59-8.76 5.25-16.92 8.31-25.22 0.05 7.9 1.97 15.72 5.63 22.72-5.13 7.89-9.04 16.52-11.95 25.46-2.07 6.57-4.57 13.31-3.58 20.32l-1.41 1.52c-0.57-14.99 0.41-30.03 3-44.8zm200.09-7.18c3.4 4.54 6.63 9.23 10.53 13.38 6.93 7.72 15.79 13.61 25.73 16.68-0.24 6.74-0.53 13.5-1.11 20.23 0.08-2.61 0.46-5.37-0.5-7.86-2.01-3.03-5.21-4.95-7.97-7.21-7.05-5.25-13-11.77-19.82-17.29-0.13 1.5-0.24 3-0.32 4.51-3.69-6.97-5.65-14.64-6.54-22.44zm8.16 135.32c10.48-0.38 20.99-1.9 30.69-6.04-1.66 3.48-3.39 6.94-5.42 10.23-2.32-0.72-4.78-0.32-7.11 0.09-7.28-0.5-14.61-0.65-21.85 0.34 1.29-1.49 2.49-3.06 3.69-4.62zm41.95 26.15c7.74-0.75 14.84-4.01 22.11-6.52-1.92 3.47-3.75 7.31-7.1 9.63-11.78 2.56-25.23 4.16-35.78-3.01 6.92 0.14 13.88 1.16 20.77-0.1zm-253.91 22.77c2.56 1.27 5.27 2.37 7.52 4.18 0.71 1.54-0.37 3.24-0.66 4.8-3.27 9.39-6.92 18.65-10.55 27.9-3.5-0.9-6.95-2-10.3-3.38 7.3-9.79 11.45-21.65 13.99-33.5zm148.33 25.52c2.49-2.24 5.77-3.24 9.08-3.21-1.52 5.8-2.37 11.73-3.58 17.59l-0.18-1.78c-8.2 1.93-16.13 5.03-24.54 6.1-16.64 2.86-35.69 1.25-48.76-10.5l1.82-9.08c6.05 3.55 11.14 8.93 18.24 10.47 16.25 5.36 34.9 1.4 47.92-9.59zm51.3 98.86c10.18-4.94 20.98-8.94 29.67-16.42 4.11 6.17 8.84 11.88 13.25 17.84-8.06 8.19-17.04 15.37-26.36 22.05-5.52-7.82-11.7-15.21-16.56-23.47z\" />\n\n        <path\n            android:fillColor=\"#fffeecdc\"\n            android:pathData=\"M274.42 243.21c2.17-1.21 5.48-0.33 6.09 2.28 0.84 3.34 0.16 8.39-3.69 9.43-6.12 0.82-6.88-9.23-2.4-11.71z\" />\n\n        <path\n            android:fillColor=\"#fffee8db\"\n            android:pathData=\"M285.43 247.27c2.46-1.35 5.98 0.05 6.35 2.95 0.74 3-2.94 6.58-5.78 4.85-2.89-1.46-2.58-5.71-0.57-7.8z\" />\n\n        <path\n            android:fillColor=\"#fffee9db\"\n            android:pathData=\"M381.31 254.07c4.01-2.09 8.16 4.33 4.36 6.81-4.01 1.67-7.47-3.92-4.36-6.81zm104.6 1.85c2.04-0.38 4.69-1.03 5.96 1.21 2.47 2.44-0.33 7.11-3.65 5.9-3.41-0.69-2.08-4.67-2.31-7.11z\" />\n\n        <path\n            android:fillColor=\"#fffeebdb\"\n            android:pathData=\"M389.72 255.65c1.32 0.57 3.17 0.36 4.11 1.6l0.38 4.2a35.48 35.48 0 0 1-3.69-1.07c-0.91-1.37-0.52-3.18-0.8-4.73z\" />\n\n        <path\n            android:fillColor=\"#fffde7da\"\n            android:pathData=\"M498.17 257.4c2.53-0.21 4.8 3.61 2.63 5.39-1.67 2.55-6.28 0.11-5.59-2.69 0.02-1.48 1.73-2.31 2.96-2.7z\" />\n\n        <path\n            android:fillColor=\"#ffffefe6\"\n            android:pathData=\"M228.01 294c5.65-9.53 14.44-16.51 22.14-24.27 2.97 8.82 5.84 17.94 11.77 25.3 2.6 3.5 6.35 5.82 9.94 8.16 6.75 5.12 14.23 9.54 22.69 11.02l0.15-1.77c0.82-4.49 1.87-8.93 2.74-13.4 6.74 4.98 13.51 10.24 21.59 12.88 9 2.92 18.81 3.3 28.06 1.3a65.74 65.74 0 0 0 4.05-0.94c1.29-0.42 2.59-0.86 3.81-1.49 1.08-5.26 0.02-10.69 0.3-16.02 8.57 7.08 16.78 15.34 27.91 18.17 6.59 2.64 13.57 4.16 20.66 4.5l-0.13-3.06c0.08-1.51 0.19-3.01 0.32-4.51 6.82 5.52 12.77 12.04 19.82 17.29 2.76 2.26 5.96 4.18 7.97 7.21 0.96 2.49 0.58 5.25 0.5 7.86-2.9 26.32-10.03 53.49-27.42 74.14-12.04-6.07-23.79-12.73-35.5-19.41 4.64-2.14 9.37-4.14 14.29-5.56 7.22-2.07 14.05-5.28 21.21-7.55 4.43-1.4 8.75-3.14 13-5.02-0.18-7.79-1.38-15.5-2.72-23.15-0.41-1.96-0.84-4.11-2.46-5.45-7.38 0.78-13.8 4.93-20.34 8.12l-55.78 28.14c-2.56 1.34-5.38 2.61-7.09 5.04-0.61 2.19-0.48 5.01 1.51 6.48 3.74 3.07 8.17 5.13 12.22 7.74l49.67 31.45c0.8 2.44 1.38 5.02 2.79 7.21 1.42 1.77 3.75 2.42 5.87 2.95 4.98 1.03 10.1 0.99 15.15 1.51 3.34 1.35 6.59 2.96 9.79 4.64 10.55 7.17 24 5.57 35.78 3.01-3.36 3.81-8.07 6.48-13.23 6.61-5.5 0.6-11.61-1.94-16.6 1.34-8.78 5.44-17.64 10.91-27.27 14.76-8.13 3.04-16 6.75-24.22 9.53-15.8 4.22-32.05 6.41-48.25 8.46-12.15 0.68-24.36 1.89-36.51 0.64-5.74-0.61-11.54-0.25-17.27-1-9.05-0.91-17.75-3.68-26.66-5.36-15.83-3.19-30.29-10.58-45.12-16.66-12.09-5.58-24.36-13.05-30.88-25.12-4.07-7.32-4.71-16.37-1.18-24.04 5.45 0.11 11.02 0.18 16.31-1.31 3.51-0.93 5.3-4.35 7.55-6.88 2.99-2.06 6.35-3.49 9.66-4.94l38.06-16.9c4.24-2.04 9.05-3.24 12.63-6.41 2.77-4.04-0.99-8.04-3.75-10.79L216.6 339.4c-3.01-2.89-5.6-6.5-9.55-8.17-4.75 2.65-7.74 7.44-11.58 11.17-0.99-7.01 1.51-13.75 3.58-20.32 2.91-8.94 6.82-17.57 11.95-25.46 1.56 2.61 3.36 5.08 5.32 7.42l1.24-0.72c1.13-0.64 2.29-1.23 3.49-1.74l-0.83 1.74c8.39-0.88 16.8 1.81 23.28 7.17 4.66 3.5 7.77 8.51 11.57 12.84-0.89-8.27-7.69-13.91-14.22-18.12-4.97-3.25-10.98-4.43-16.85-4.12 1.3-2.38 2.59-4.78 4.01-7.09m120.04 39.18c3.73-2.35 7.1-5.29 11.18-7.06 8.05-3.52 17.03-5.27 25.8-4.13 4.85 0.59 9.5 2.14 14.32 2.88-6.89-6.59-17.3-5.83-26.1-5.38-9.5 1.33-20.01 5.07-25.2 13.69m-79.88 109.9c-0.24 0.87-0.49 1.74-0.65 2.64-2.11 4.06-2.24 8.77-1.38 13.19 1.91 11.69 9.07 23.36 20.72 27.34 9.17 3 20.59 2.49 27.91-4.48 6.54-6.57 8.43-16.79 6-25.56-1.25-4.34-3.96-8.04-6.15-11.93-15.36-2.1-31.05-4.04-46.45-1.2zm-73.11-88.21c5.97 3.95 11.42 8.6 16.82 13.28 4.96 4.34 10.51 7.93 15.66 12.03-5.27 2.27-10.6 4.44-16.09 6.12-2.66 0.89-4.62-1.86-6.31-3.46-6.49-7.8-8.91-18.11-10.08-27.97zM66.8 389.11c2.94-4.44 10.09-7.37 14.56-3.45 1.32 1.77 1.15 4.18 1.65 6.26 1 8.31 5.08 15.73 7.89 23.49-5.68-0.38-11.23 1.09-16.35 3.46-4.23-9.31-8.31-19.38-7.75-29.76zm29.82 2.48c0.88-2.48 1.54-5.91 4.51-6.65 2.89-0.79 6.82-0.33 8.47 2.49 0.16 3.42-0.86 6.74-1.35 10.1-1.08 5.69-0.77 11.5-0.49 17.25-4.34-0.27-8.7 0.05-12.97 0.87-0.96-8.03-0.16-16.25 1.83-24.06zm90.63 1.98l4.77 3.23c-0.1 4.56 0.42 9.12 1.34 13.59-3.97-0.28-7.84-1.18-11.72-2.03 1.61-5.02 3.27-10.05 5.61-14.79zM58.33 427.31c2.77-2.06 6.31-2.06 9.59-2.24-0.81 5.58-0.99 11.25-0.12 16.84-1.53 1.74-3.42 3.29-4.34 5.47-0.74 2.46-0.71 5.07-0.88 7.62-3.48 0.63-8.54 1.87-10.23-2.38-3.39-8.39-2.37-20.18 5.98-25.31zm402.88 35.97c1.83-2.53 4.14-4.67 6.78-6.34-0.65 1.32-1.24 2.68-2.08 3.89l-4.7 2.45zm-302.03-4.41l34.17 17.31c-2.54 11.85-6.69 23.71-13.99 33.5-11.61-5.21-23.96-8.8-35.15-14.93 4.92-11.99 10.6-23.66 14.97-35.88zm-98.87 2.42c1-2.24 3.77-0.75 5.3 0.1 2.93 2.08 6.46 4.65 6.63 8.59 0.31 4.31 1.31 8.52 2.73 12.59-5.5-0.96-11.11-3.47-14.13-8.39-2.15-3.84-3.03-9.01-0.53-12.89zm209.88 76.04c0.14-9.24 1.24-18.47 3.51-27.43 13.07 11.75 32.12 13.36 48.76 10.5 8.41-1.07 16.34-4.17 24.54-6.1l0.18 1.78c-0.44 7.71-1.57 15.39-2.69 23.03-10.18-0.06-20.38-0.46-30.5-1.57-5.67-0.27-11.34-0.15-16.99-0.74-8.95-0.83-17.85 1.25-26.81 0.53zm139.35 86.7c9.32-6.68 18.3-13.86 26.36-22.05l27.55 34.58c2.5 2.68 4.65 5.64 6.57 8.77-10.83 6.2-21.09 13.5-29.79 22.5-10.88-14.13-20.57-29.13-30.69-43.8z\" />\n\n        <path\n            android:fillColor=\"#fffeae94\"\n            android:pathData=\"M290.13 281.94c9.78 21.15 33.9 34.22 56.96 31.28-9.25 2-19.06 1.62-28.06-1.3-8.08-2.64-14.85-7.9-21.59-12.88l-2.74 13.4c-5.06-9.33-5.28-20.17-4.57-30.5z\" />\n\n        <path\n            android:fillColor=\"#ff741f2c\"\n            android:pathData=\"M221.05 301.58l2.95-0.49c5.87-0.31 11.88 0.87 16.85 4.12 6.53 4.21 13.33 9.85 14.22 18.12-3.8-4.33-6.91-9.34-11.57-12.84-6.48-5.36-14.89-8.05-23.28-7.17l0.83-1.74z\" />\n\n        <path\n            android:fillColor=\"#ff701926\"\n            android:pathData=\"M348.05 333.18c5.19-8.62 15.7-12.36 25.2-13.69 8.8-0.45 19.21-1.21 26.1 5.38-4.82-0.74-9.47-2.29-14.32-2.88-8.77-1.14-17.75 0.61-25.8 4.13-4.08 1.77-7.45 4.71-11.18 7.06z\" />\n\n        <path\n            android:fillColor=\"#ffd36c91\"\n            android:pathData=\"M108.36 324.49l2.11 0.54-0.28 17.9-6.59-1.25c0.63-3.67 1.34-7.32 2.17-10.95l3.21-0.89-0.62-5.35z\" />\n\n        <path\n            android:fillColor=\"#fffeeedd\"\n            android:pathData=\"M428.36 380.38c3.19-9.78 5.08-19.91 6.78-30.04 3.23 15.85-0.72 32.83-9.5 46.31-1.91 3.55-4.81 6.38-7.92 8.89 4.04-8.16 8-16.42 10.64-25.16z\" />\n\n        <path\n            android:fillColor=\"#ffffb095\"\n            android:pathData=\"M180.74 390.43c4.2 0.8 8.44 1.36 12.7 1.72-0.52 1.53-1.02 3.07-1.42 4.65l-4.77-3.23c-2.34 4.74-4 9.77-5.61 14.79l-0.72-0.03a296.79 296.79 0 0 1-0.18-17.9z\" />\n\n        <path\n            android:fillColor=\"#ffff927c\"\n            android:pathData=\"M168.67 408.54l7.56-0.27 4.69 0.06 0.72 0.03c3.88 0.85 7.75 1.75 11.72 2.03 1.61 2.55 3.59 4.83 5.58 7.1-2.25 2.53-4.04 5.95-7.55 6.88-5.29 1.49-10.86 1.42-16.31 1.31-1.65-0.08-3.29-0.31-4.89-0.72-3.33-1.09-7.13-1.93-9.52-4.69-2.29-4.06 0.98-8.14 3.15-11.42l4.85-0.31zm232.95 23.34c7.24-0.99 14.57-0.84 21.85-0.34 1.77 0.92 3.53 1.89 5.2 3.02l2.33 1.81c-0.67 2.62-0.33 6.05-2.85 7.73-2.84 1.94-6.11 3.13-9.33 4.28l-2.12 0.49c-5.05-0.52-10.17-0.48-15.15-1.51-2.12-0.53-4.45-1.18-5.87-2.95-1.41-2.19-1.99-4.77-2.79-7.21 2.85-1.87 5.75-3.66 8.73-5.32z\" />\n\n        <path\n            android:fillColor=\"#fffff0e7\"\n            android:pathData=\"M118.15 410.19c2.24-1.73 5.13-0.47 7.62-0.18 0.14 3.84-0.26 7.71-1.63 11.33l-10.81-3.42c1.15-2.82 2.07-6.02 4.82-7.73z\" />\n\n        <path\n            android:fillColor=\"#ff89defe\"\n            android:pathData=\"M96.02 417.69c4.28-0.05 8.95-0.64 12.87 1.48 0.97 1.5 1.39 3.28 2.03 4.95-3.96-0.58-7.95-0.92-11.95-0.78l-0.77 0.02-2.18-5.67z\" />\n\n        <path\n            android:fillColor=\"#ff8adeff\"\n            android:pathData=\"M76.94 422.03c3.79-4.08 10.07-3.34 15.15-3.56l3.88 6.41c-5.35-0.22-10.69 0.5-15.93 1.53l-0.82 0.15c-0.77-1.51-1.61-2.98-2.28-4.53z\" />\n\n        <path\n            android:fillColor=\"#ff89dfff\"\n            android:pathData=\"M112.83 420.57c3.72 0.95 7.34 2.31 10.71 4.16l-0.43 5.78c-3.06-1.69-6.07-3.45-9.07-5.22-0.4-0.24-1.21-0.7-1.61-0.93l0.4-3.79zM75.2 467.25l4.81-2.82c1.03 6.36 3.51 12.33 6.26 18.11-1.97-0.07-3.96 0.6-5.91 0.08-4.52-3.65-5.08-9.97-5.16-15.37z\" />\n\n        <path\n            android:fillColor=\"#ff8adfff\"\n            android:pathData=\"M70.79 426.01l6.89 3.93-0.05 0.8c-0.48 5.62-0.39 11.27 0.37 16.86-2.21-1.58-4.72-3-6.22-5.34-2.34-5.09-1.86-10.88-0.99-16.25z\" />\n\n        <path\n            android:fillColor=\"#fffed6c7\"\n            android:pathData=\"M423.47 431.54c2.33-0.41 4.79-0.81 7.11-0.09l-1.91 3.11c-1.67-1.13-3.43-2.1-5.2-3.02z\" />\n\n        <path\n            android:fillColor=\"#ffd26b91\"\n            android:pathData=\"M476.85 445c0.61-3.62 2.75-6.6 6.13-8.1-1.22 3.25-3.38 6.01-6.13 8.1zm40.56 92.62c5.14 12.15 0.83 25.42 3.39 38.01-0.28 1.56-0.72 3.09-1.12 4.63-1.09-7.02-2.63-13.97-5.44-20.52-3.1-7.39-0.54-15.49 3.17-22.12z\" />\n\n        <path\n            android:fillColor=\"#ff751f2c\"\n            android:pathData=\"M268.17 443.08c15.4-2.84 31.09-0.9 46.45 1.2 2.19 3.89 4.9 7.59 6.15 11.93 2.43 8.77 0.54 18.99-6 25.56-7.32 6.97-18.74 7.48-27.91 4.48-11.65-3.98-18.81-15.65-20.72-27.34-0.86-4.42-0.73-9.13 1.38-13.19-0.08 6.91 0.96 13.87 3.43 20.34 3.5 9.11 11.36 16.94 21.25 18.52 6.81 1 14.77 0.68 20.05-4.33 6.78-6.54 8.05-17.08 5.32-25.78-1.15-3.02-2.37-7.07-5.95-7.89-5.06-1.06-10.24-1.32-15.36-1.87-8.59-0.8-17.21-0.45-25.81 0l-2.28-1.63z\" />\n\n        <path\n            android:fillColor=\"#ffc05552\"\n            android:pathData=\"M267.52 445.72c0.16-0.9 0.41-1.77 0.65-2.64l2.28 1.63c8.6-0.45 17.22-0.8 25.81 0-3.64-0.16-7.69-0.57-10.84 1.66-6.86 4.74-11.58 11.96-14.47 19.69-2.47-6.47-3.51-13.43-3.43-20.34z\" />\n\n        <path\n            android:fillColor=\"#fff1a79a\"\n            android:pathData=\"M285.42 446.37c3.15-2.23 7.2-1.82 10.84-1.66 5.12 0.55 10.3 0.81 15.36 1.87 3.58 0.82 4.8 4.87 5.95 7.89 2.73 8.7 1.46 19.24-5.32 25.78-5.28 5.01-13.24 5.33-20.05 4.33-9.89-1.58-17.75-9.41-21.25-18.52 2.89-7.73 7.61-14.95 14.47-19.69z\" />\n\n        <path\n            android:fillColor=\"#ffd3d6e1\"\n            android:pathData=\"M78 447.6c3.56 1.19 7.68 3.25 7.74 7.59 0.92 4.23-2.75 7.06-5.73 9.24 0.55-2.45 1.33-4.9 1.29-7.44-0.22-3.38-1.87-6.4-3.3-9.39z\" />\n\n        <path\n            android:fillColor=\"#ffd3d6e2\"\n            android:pathData=\"M157.1 450.76c0.5 1.46 0.84 3.06 0.26 4.56-2.31 7.59-5.92 14.7-9.08 21.96-3.37 7.33-6.38 14.87-10.7 21.72-1.67-0.88-3.43-1.58-5.15-2.34 8.08-10.16 13.24-22.24 19.85-33.34 2.43-3.83 3.82-8.17 4.82-12.56zm279.49 17.26c5.01-0.16 10.03-1.25 15.03-0.69 3.32 2.24 5.29 5.88 7.69 9-5.11-1.57-10.52-1.83-15.74-0.63l-1.47-1.75c-1.77-2.05-3.56-4.07-5.51-5.93zm-175.71 29.94c4.2 0.83 8.41 1.67 12.52 2.9l-1.81 8.07c-3.07-4.12-6.86-7.6-10.71-10.97zm48.12 3.86c9.21 0.12 18.38-0.94 27.58-0.9-8.29 6.54-19.2 9.37-29.63 9.19-8.17-1.01-17.24-2.12-23.45-8.08 8.5-0.38 17 0.44 25.5-0.21z\" />\n\n        <path\n            android:fillColor=\"#ff6f96cc\"\n            android:pathData=\"M430.98 470.42c1.76-0.89 3.54-2.95 5.61-2.4 1.95 1.86 3.74 3.88 5.51 5.93l-4.2 4.33c-2.21-2.71-4.55-5.3-6.92-7.86zm-9.72 6.28l4.48-2.79 7.45 11.52-3.72 4.83c-2.51-4.65-5.24-9.19-8.21-13.56zm-13.16 6.85l5.73-2.33c2.99 5.69 6.17 11.28 9.72 16.64l-4.38 5.53c-4.11-6.37-7.46-13.18-11.07-19.84z\" />\n\n        <path\n            android:fillColor=\"#ffd2d5e1\"\n            android:pathData=\"M425.74 473.91l5.24-3.49c2.37 2.56 4.71 5.15 6.92 7.86l1.12 1.39a252.34 252.34 0 0 0-5.44 6.34l-0.39-0.58c-2.55-3.8-4.99-7.67-7.45-11.52zm-11.91 7.31l7.43-4.52c2.97 4.37 5.7 8.91 8.21 13.56l0.34 0.62a203.1 203.1 0 0 0-5.9 7.52l-0.36-0.54c-3.55-5.36-6.73-10.95-9.72-16.64zm-12.77 5.84c1.76-2 4.68-2.44 7.04-3.51 3.61 6.66 6.96 13.47 11.07 19.84l0.34 0.51c-2.74 3.06-5.1 6.44-7.14 10-4.16-8.76-8.67-17.48-11.31-26.84zm-159.12 48.66c3.6 3.91 6.61 8.31 9.15 12.97-0.89 3.37 0.65 6.71 1.75 9.83-3.11-0.66-6.3-1.4-9.5-0.91-6.82 1.02-13.26 4.22-20.24 4.18-5.45-0.15-9.27-4.6-12.21-8.67 3.03 1.48 6.46 2.58 9.85 1.72 8.14-1.78 15.95-4.82 23.65-7.98a75.83 75.83 0 0 1-2.45-11.14zm148.47 38.58c4.98-3.47 11.32-2.33 17.02-2.48-2.71 2.31-5.77 4.15-8.94 5.76-5.19 2.63-10.11 6.01-15.88 7.28 2.02-3.88 3.95-8.14 7.8-10.56z\" />\n\n        <path\n            android:fillColor=\"#ff89deff\"\n            android:pathData=\"M442.1 473.95l1.47 1.75c4.97 5.98 9.25 12.47 13.57 18.92-1.95 0.75-3.86 1.57-5.76 2.42-3.84-5.99-7.92-11.81-12.36-17.37l-1.12-1.39 4.2-4.33zm-12.63 16.31l3.72-4.83 0.39 0.58 9.59 14.1-6.99 2.58c-2.06-3.98-4.2-7.9-6.37-11.81l-0.34-0.62zm-5.92 7.6l0.36 0.54 5.48 7.74-4.32 5.11-5.56-7.35-0.34-0.51 4.38-5.53z\" />\n\n        <path\n            android:fillColor=\"#ff585d7e\"\n            android:pathData=\"M203.58 480.05c3.58 0.89 8.24 0.77 10.57 4.04 0.76 5.66-0.95 11.28-2.3 16.72-3.22 11.43-7.06 22.82-12.93 33.19l-3.9-0.03-0.01-0.87c-0.05-7.51-0.07-15.03-0.44-22.53 0.05-2.47 1.13-4.76 1.99-7.03 3.1-7.57 5.58-15.42 7.02-23.49z\" />\n\n        <path\n            android:fillColor=\"#fffdfdfe\"\n            android:pathData=\"M423.91 498.4l5.9-7.52 6.37 11.81-6.79 3.45-5.48-7.74z\" />\n\n        <path\n            android:fillColor=\"#ffd4d7e3\"\n            android:pathData=\"M203.73 532.96c5.36-13.32 10.77-26.86 12.78-41.16 0.57 5.18 2 10.42 0.95 15.64-1.28 6.02-4.26 11.49-6.93 16.98-1.56 3.35-4.19 5.99-6.8 8.54z\" />\n\n        <path\n            android:fillColor=\"#ffd2d6e2\"\n            android:pathData=\"M353.58 498.45c3.36-0.7 6.96-1.86 10.29-0.36 6.08 2.41 12.31 4.7 17.89 8.19 5.62 3.91 11.12 8.2 15.3 13.7 2.97 3.59 5.4 7.94 9.65 10.2 3.91 2.09 8 3.93 11.49 6.72-5.1-0.39-10.57 0.5-15.34-1.74-4.56-3.63-5.6-9.8-8.32-14.67-1.77-3.87-5.74-5.84-9.29-7.75-4.28-2.17-8.5-4.61-13.15-5.89-2.97-0.93-5.93 0.45-8.59 1.65-4.22 2.02-8.45 4.04-12.85 5.67a144.46 144.46 0 0 1 2.92-15.72z\" />\n\n        <path\n            android:fillColor=\"#ff7e7d9e\"\n            android:pathData=\"M194.57 510.57c0.37 7.5 0.39 15.02 0.44 22.53-2.8-0.8-5.51-1.85-8.15-3.06 2.23-6.62 4.43-13.29 7.71-19.47z\" />\n\n        <path\n            android:fillColor=\"#fffce4d9\"\n            android:pathData=\"M135.2 533.29l1.22-12.54c2.1 15.31 0 31.21-6.39 45.31l0.17-2.06c0.21-2.49 0.9-4.88 1.55-7.26 2.1-7.65 2.68-15.59 3.45-23.45z\" />\n\n        <path\n            android:fillColor=\"#ff7f7fa0\"\n            android:pathData=\"M274.8 539.74l9.97-0.12c-0.53 6.36-0.98 12.74-1.06 19.14-1.03 13.48 0.69 26.96 0 40.46-0.32-6.74-0.4-13.48-0.62-20.22l-10.31-1.12c-0.2-12.73 0.05-25.54 2.02-38.14zm14.54-0.09c12.88 0.03 25.73 1.1 38.58 1.82 5.99 0.39 11.99-0.18 17.98 0.01l-5.75 0.44-0.02 1.49c-1.99 12.24-0.2 24.66 0 36.96-5.12 0.21-10.26 0.07-15.37-0.37 0.06-4.76-0.27-9.5-0.47-14.25l-0.16-24.01-4.28 0.08c-1.66 12.35-0.74 24.82-0.76 37.23l0.02 0.94c-4.48-0.29-9.35-0.99-13.29 1.71l0.1-3.58 0.23-37.45-4.89 0.26-0.08 36.1 0.02 2.72c-4.08-0.54-8.2-0.8-12.31-0.84l0.04-1.7c-0.08-12.53 0.62-25.04 0.41-37.56zm128.74 30.24c1.5-1.84 3.6-3.02 5.59-4.25 1.51 3.12 2.95 6.28 4.36 9.45-6.22 6.7-13.91 11.81-22.04 15.89-5.84 2.86-11.47 6.24-17.7 8.22 8.99-10.7 21.22-18.2 29.79-29.31zm-59.92 82.4L386.88 642c4.46 9.21 8.25 18.73 12.6 28h-35.2c-2.38-5.78-4.08-11.81-6.12-17.71zM259.95 654l8.32 3.96-0.23 12.04h-8.26c-0.16-5.34-0.12-10.67 0.17-16z\" />\n\n        <path\n            android:fillColor=\"#ff81c8ef\"\n            android:pathData=\"M284.77 539.62l4.57 0.03c0.21 12.52-0.49 25.03-0.41 37.56l-4.54-0.32c-0.09-6.05-0.49-12.09-0.68-18.13 0.08-6.4 0.53-12.78 1.06-19.14z\" />\n\n        <path\n            android:fillColor=\"#ff80c8f0\"\n            android:pathData=\"M301.26 540.93l4.89-0.26c0.14 12.48 0 24.97-0.23 37.45l-4.74-1.09c0.24-12.04 0.12-24.07 0.08-36.1z\" />\n\n        <path\n            android:fillColor=\"#ff81c7ef\"\n            android:pathData=\"M319.85 541.82l4.28-0.08c0.21 8 0.08 16.01 0.16 24.01-0.09 5.11-0.42 10.22-0.62 15.33l-4.58-2.03c0.02-12.41-0.9-24.88 0.76-37.23z\" />\n\n        <path\n            android:fillColor=\"#ffdadce6\"\n            android:pathData=\"M340.97 546.79c2.13 11.09 1.1 22.48 2.43 33.67 4.81-0.15 10.94-2.01 14.4 2.46 1.53 2.13 1.98 5.66 5.01 6.18 4.08 1.28 10.01 0.38 11.75 5.37 1.23 5.34 0.15 10.95 0.62 16.41-10.33-3.4-20.97-5.56-31.75-6.82-0.45-19.1-2.24-38.15-2.46-57.27z\" />\n\n        <path\n            android:fillColor=\"#ff78b1de\"\n            android:pathData=\"M283.71 599.22c0.69-13.5-1.03-26.98 0-40.46l0.68 18.13 4.54 0.32-0.04 1.7c-0.75 12.48 1.23 24.87 1.93 37.3-2.44-2.11-5.41-4.12-6.5-7.3-0.37-3.22-0.51-6.46-0.61-9.69z\" />\n\n        <path\n            android:fillColor=\"#ff78b0de\"\n            android:pathData=\"M324.29 565.75c0.2 4.75 0.53 9.49 0.47 14.25-0.22 8.26 0.72 16.48 1.24 24.71l-5.54 3.73c-0.94-9.45-0.83-18.97-1.35-28.45l-0.02-0.94 4.58 2.03 0.62-15.33z\" />\n\n        <path\n            android:fillColor=\"#ffd9dce6\"\n            android:pathData=\"M269.89 580.58l0.94-12.8c-0.41 11.43-0.47 22.89 0.37 34.31l-11.47 0.36c-0.21-7.51-0.19-15.02-0.05-22.53l10.21 0.66z\" />\n\n        <path\n            android:fillColor=\"#ff585c7e\"\n            android:pathData=\"M394.55 583.49c8.14-4.03 15.55-9.31 23.53-13.6-8.57 11.11-20.8 18.61-29.79 29.31-1.33 1.5-3.29 2.14-5.05 2.98-2.43-2.99-4.71-6.11-6.72-9.4 5.79-3.52 12.12-6 18.03-9.29z\" />\n\n        <path\n            android:fillColor=\"#ff7ab5e2\"\n            android:pathData=\"M301.18 577.03l4.74 1.09-0.1 3.58c0.54 11.81 0.98 23.63 2.13 35.41-0.56 2.39-3.42 3.48-5.16 5.03-2.13-14.04-1.24-28.25-1.59-42.39l-0.02-2.72z\" />\n\n        <path\n            android:fillColor=\"#ff5e6283\"\n            android:pathData=\"M272.78 577.88l10.31 1.12 0.62 20.22 0.61 9.69c-3.52-2.12-7.18-4.14-10.16-6.99-1.46-1.19-1.24-3.23-1.44-4.89-0.28-6.38-0.09-12.77 0.06-19.15z\" />\n\n        <path\n            android:fillColor=\"#ff626687\"\n            android:pathData=\"M288.89 578.91c4.11 0.04 8.23 0.3 12.31 0.84 0.35 14.14-0.54 28.35 1.59 42.39-0.59 1.78-2.43 2.76-3.59 4.16-2.74-3.39-6.47-6.13-8.38-10.09-0.7-12.43-2.68-24.82-1.93-37.3zm16.93 2.79c3.94-2.7 8.81-2 13.29-1.71 0.52 9.48 0.41 19 1.35 28.45-4.09 3.01-7.81 6.6-12.51 8.67-1.15-11.78-1.59-23.6-2.13-35.41z\" />\n\n        <path\n            android:fillColor=\"#ff606485\"\n            android:pathData=\"M324.76 580c5.11 0.44 10.25 0.58 15.37 0.37-0.1 7.96 1.03 15.86 1.24 23.81-5.13 0.14-10.26 0.16-15.37 0.53-0.52-8.23-1.46-16.45-1.24-24.71z\" />\n\n        <path\n            android:fillColor=\"#ff6f9acf\"\n            android:pathData=\"M259.79 604.86l3.44-0.15c-0.77 13.22-2.39 26.38-3.14 39.61-0.55-13.14-0.51-26.31-0.3-39.46z\" />\n\n        <path\n            android:fillColor=\"#ff83ccf3\"\n            android:pathData=\"M263.23 604.71c5.15-0.05 11.27-1.28 15.4 2.63 5.23 4.71 9.68 10.2 14.22 15.55-5.97 11.75-10.19 24.28-15.81 36.18-5.38-2.95-11.3-4.7-16.71-7.55-0.82-2.31-0.34-4.82-0.24-7.2 0.75-13.23 2.37-26.39 3.14-39.61z\" />\n\n        <path\n            android:fillColor=\"#ff81c8f0\"\n            android:pathData=\"M323.38 609.36c2.89-1.82 6.01-3.7 9.55-3.64 5.83-0.1 11.64 0.72 17.33 1.96-7.88-0.25-11.74 7.81-18.7 9.89-6.61 2.18-13.57 3.73-20.56 3.51l-2-0.88c4.31-4.21 9.34-7.58 14.38-10.84z\" />\n\n        <path\n            android:fillColor=\"#ff87d6fa\"\n            android:pathData=\"M331.56 617.57c6.96-2.08 10.82-10.14 18.7-9.89 8.52 0.6 16.8 3.17 24.77 6.14-0.12 5.16-0.88 10.51 0.68 15.54 1.71 3.48 5.56 5.13 8.27 7.73l1.96 1.94c-3.62 1.89-7.42 3.39-11.3 4.61-15.49 5.04-30.75 10.89-46.62 14.67-4.86-6.23-7.2-13.84-10.35-20.96-2.2-5.44-4.99-10.64-6.67-16.27 6.99 0.22 13.95-1.33 20.56-3.51z\" />\n\n        <path\n            android:fillColor=\"#ff6e97cd\"\n            android:pathData=\"M375.03 613.82c1.03 8.47 6.93 15.12 8.95 23.27-2.71-2.6-6.56-4.25-8.27-7.73-1.56-5.03-0.8-10.38-0.68-15.54z\" />\n\n        <path\n            android:fillColor=\"#ffd4d8e4\"\n            android:pathData=\"M307.32 620.56c4.37 8.16 7.5 16.91 10.77 25.55-2.86-3.51-6.06-6.76-9.68-9.49-1.2-5.27-1.54-10.68-1.09-16.06z\" />\n\n        <path\n            android:fillColor=\"#ffd3d7e3\"\n            android:pathData=\"M284.83 647.77c2.85-6.75 5.21-13.72 8.73-20.16 1.25 5.1 0.92 10.53-2.14 14.96-4.59 6.43-8.24 13.46-12.59 20.05 1.82-5.03 3.92-9.94 6-14.85z\" />\n\n        <path\n            android:fillColor=\"#ffffded9\"\n            android:pathData=\"M298.13 637.94c3.78-0.13 2.42 3.68 2.01 6l-3.48-0.04c0.25-2.02-0.02-4.35 1.47-5.96z\" />\n\n        <path\n            android:fillColor=\"#ffd1d5e1\"\n            android:pathData=\"M442.62 670c5.17-4.73 10.4-9.42 16.36-13.15-3.12 4.42-5.89 9.11-9.52 13.15h-6.84z\" />\n\n    </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/cdn_speedtest_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/upos_name\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"3\"\n        android:padding=\"5sp\"\n        android:singleLine=\"true\"\n        android:textSize=\"14sp\" />\n\n    <TextView\n        android:id=\"@+id/upos_speed\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"2\"\n        android:padding=\"5sp\"\n        android:singleLine=\"true\"\n        android:textSize=\"14sp\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/custom_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_columnWeight=\"1\"\n            android:singleLine=\"false\"\n            android:text=\"ID必填，其他不填则表示不替换\"\n            android:textSize=\"18sp\"\n            tools:ignore=\"HardcodedText\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"1\"\n                android:singleLine=\"true\"\n                android:text=\"替换项目ID\"\n                tools:ignore=\"HardcodedText\" />\n\n            <EditText\n                android:id=\"@+id/custom_button_id\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"4\"\n                android:layout_weight=\"1\"\n                android:digits=\"0123456789\"\n                android:hint=\"ID必须存在，且标题项目也存在\"\n                android:inputType=\"text\"\n                android:tag=\"custom_button_id\"\n                tools:ignore=\"HardcodedText\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"1\"\n                android:singleLine=\"true\"\n                android:text=\"新名称\"\n                tools:ignore=\"HardcodedText\" />\n\n            <EditText\n                android:id=\"@+id/custom_button_title\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"4\"\n                android:layout_weight=\"1\"\n                android:tag=\"custom_button_title\" />\n        </LinearLayout>\n\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"1\"\n                android:singleLine=\"true\"\n                android:text=\"新链接\"\n                tools:ignore=\"HardcodedText\" />\n\n            <EditText\n                android:id=\"@+id/custom_button_uri\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"4\"\n                android:layout_weight=\"1\"\n                android:hint=\"bilibili://user_center/vip\"\n                android:tag=\"custom_button_uri\"\n                tools:ignore=\"HardcodedText\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"1\"\n                android:singleLine=\"true\"\n                android:text=\"新图标\"\n                tools:ignore=\"HardcodedText\" />\n\n            <EditText\n                android:id=\"@+id/custom_button_icon\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_columnWeight=\"4\"\n                android:hint=\"https://i0.hdslb.com/bfs/album/276769577d2a5db1d9f914364abad7c5253086f6.png\"\n                android:tag=\"custom_button_icon\"\n                tools:ignore=\"HardcodedText\" />\n        </LinearLayout>\n\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/custom_subtitle_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingLeft=\"16dp\"\n        android:paddingTop=\"10dp\"\n        android:paddingRight=\"16dp\"\n        android:paddingBottom=\"10dp\">\n\n        <RelativeLayout\n            android:id=\"@+id/pvBlack\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignBottom=\"@id/tv_pvBlack\"\n                android:layout_marginBottom=\"-5dp\"\n                android:background=\"#000\" />\n\n            <TextView\n                android:id=\"@+id/tv_pvBlack\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerInParent=\"true\"\n                android:layout_margin=\"5dp\"\n                android:gravity=\"center\" />\n        </RelativeLayout>\n\n        <RelativeLayout\n            android:id=\"@+id/pvWhite\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignBottom=\"@id/tv_pvWhite\"\n                android:layout_marginBottom=\"-5dp\"\n                android:background=\"#FFF\" />\n\n            <TextView\n                android:id=\"@+id/tv_pvWhite\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerInParent=\"true\"\n                android:layout_margin=\"5dp\"\n                android:gravity=\"center\" />\n        </RelativeLayout>\n\n        <RelativeLayout\n            android:id=\"@+id/pvTp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <ImageView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignBottom=\"@id/tv_pvTp\"\n                android:layout_marginBottom=\"-5dp\"\n                android:scaleType=\"center\"\n                android:src=\"@drawable/tp\"\n                tools:ignore=\"ContentDescription\" />\n\n            <TextView\n                android:id=\"@+id/tv_pvTp\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerInParent=\"true\"\n                android:layout_margin=\"5dp\"\n                android:gravity=\"center\" />\n        </RelativeLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_noBg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"10dp\"\n                android:layout_weight=\"1\"\n                android:ellipsize=\"end\"\n                android:paddingTop=\"8dp\"\n                android:paddingBottom=\"8dp\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_remove_bg\"\n                android:textSize=\"16sp\" />\n\n            <Switch\n                android:id=\"@+id/noBg\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@android:color/transparent\"\n                android:clickable=\"false\"\n                android:tag=\"subtitle_remove_bg\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_bold\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"?android:attr/selectableItemBackground\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"10dp\"\n                android:layout_weight=\"1\"\n                android:ellipsize=\"end\"\n                android:paddingTop=\"8dp\"\n                android:paddingBottom=\"8dp\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_bold\"\n                android:textSize=\"16sp\" />\n\n            <Switch\n                android:id=\"@+id/bold\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@android:color/transparent\"\n                android:clickable=\"false\"\n                android:tag=\"subtitle_bold\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_font\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <LinearLayout\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:orientation=\"vertical\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:text=\"@string/custom_subtitle_font\"\n                    android:textSize=\"16sp\" />\n\n                <TextView\n                    android:id=\"@+id/tv_fontStatus\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:ellipsize=\"end\"\n                    android:singleLine=\"true\"\n                    android:textColor=\"?android:attr/textColorSecondary\"\n                    android:textSize=\"12sp\" />\n            </LinearLayout>\n\n            <Button\n                android:id=\"@+id/btn_resetFont\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/custom_subtitle_reset\" />\n\n            <Button\n                android:id=\"@+id/btn_importFont\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/custom_subtitle_import\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_fill_color\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/font_color\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789ABCDEFabcdef\"\n                android:inputType=\"text|textCapCharacters\"\n                android:tag=\"subtitle_font_color2\"\n                tools:ignore=\"LabelFor\" />\n\n            <Button\n                android:id=\"@+id/btn_chooseColorFc\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/custom_subtitle_pick_color\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_bg_color\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/background_color\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789ABCDEFabcdef\"\n                android:inputType=\"text|textCapCharacters\"\n                android:tag=\"subtitle_background_color\"\n                tools:ignore=\"LabelFor\" />\n\n            <Button\n                android:id=\"@+id/btn_chooseColorBc\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/custom_subtitle_pick_color\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_size\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_font_size\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/font_size\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789\"\n                android:inputType=\"number\"\n                android:tag=\"subtitle_font_size\"\n                tools:ignore=\"LabelFor\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_sizePortrait\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_font_size_portrait\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/fontSizePortrait\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789\"\n                android:hint=\"@string/custom_subtitle_zero_as_default\"\n                android:inputType=\"number\"\n                android:tag=\"subtitle_font_size_portrait\"\n                tools:ignore=\"LabelFor\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_sizeLandscape\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_font_size_landscape\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/fontSizeLandscape\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789\"\n                android:hint=\"@string/custom_subtitle_zero_as_default\"\n                android:inputType=\"number\"\n                android:tag=\"subtitle_font_size_landscape\"\n                tools:ignore=\"LabelFor\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_blur\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_blur\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/font_blur_solid\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789\"\n                android:inputType=\"text|textCapCharacters\"\n                android:tag=\"subtitle_blur_solid\"\n                tools:ignore=\"LabelFor\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_stroke_color\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/stroke_color\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:autofillHints=\"yes\"\n                android:digits=\"0123456789ABCDEFabcdef\"\n                android:inputType=\"text|textCapCharacters\"\n                android:tag=\"subtitle_stroke_color\"\n                tools:ignore=\"LabelFor\" />\n\n            <Button\n                android:id=\"@+id/btn_chooseColorSc\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/custom_subtitle_pick_color\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_stroke_width\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/stroke_width\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:autofillHints=\"yes\"\n                android:inputType=\"numberDecimal\"\n                android:tag=\"subtitle_stroke_width\"\n                tools:ignore=\"LabelFor\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_subOffset\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_offset\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/subOffset\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:autofillHints=\"yes\"\n                android:digits=\"+-0123456789\"\n                android:hint=\"@string/custom_subtitle_offset_hint\"\n                android:inputType=\"number\"\n                android:tag=\"subtitle_offset\"\n                tools:ignore=\"LabelFor\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:id=\"@+id/ll_pv\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:singleLine=\"true\"\n                android:text=\"@string/custom_subtitle_test\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/et_testText\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:importantForAutofill=\"no\"\n                android:inputType=\"text\"\n                android:text=\"@string/custom_subtitle_test_text\"\n                tools:ignore=\"LabelFor\" />\n\n            <Button\n                android:id=\"@+id/btn_pv\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/custom_subtitle_preview\" />\n        </LinearLayout>\n\n        <CheckBox\n            android:id=\"@+id/cb_fixBreak\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:checked=\"false\"\n            android:tag=\"subtitle_fix_break\"\n            android:text=\"@string/custom_subtitle_fix_break\" />\n\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/customize_backup_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<GridLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:rowCount=\"4\"\n    android:columnCount=\"2\"\n    android:orientation=\"horizontal\"\n    android:paddingStart=\"25dp\"\n    android:paddingEnd=\"20dp\">\n\n\n    <TextView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"1\"\n        android:text=\"中国大陆\"\n        android:singleLine=\"true\"\n        tools:ignore=\"HardcodedText\" />\n\n    <EditText\n        android:id=\"@+id/cn_server\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"4\"\n        android:hint=\"cn.biliroaming.example.com\"\n        android:tag=\"cn_server\"\n        android:inputType=\"textUri\"\n        android:autofillHints=\"no\"\n        tools:ignore=\"HardcodedText\" />\n\n    <TextView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"1\"\n        android:text=\"香港\"\n        tools:ignore=\"HardcodedText\" />\n\n    <EditText\n        android:id=\"@+id/hk_server\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"4\"\n        android:hint=\"hk.biliroaming.example.com\"\n        android:tag=\"hk_server\"\n        android:inputType=\"textUri\"\n        android:autofillHints=\"no\"\n        tools:ignore=\"HardcodedText\" />\n\n    <TextView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"1\"\n        android:text=\"台湾\"\n        tools:ignore=\"HardcodedText\" />\n\n    <EditText\n        android:id=\"@+id/tw_server\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"4\"\n        android:hint=\"tw.biliroaming.example.com\"\n        android:tag=\"tw_server\"\n        android:inputType=\"textUri\"\n        android:autofillHints=\"no\"\n        tools:ignore=\"HardcodedText\" />\n\n    <TextView\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"1\"\n        android:text=\"泰国/东南亚\"\n        tools:ignore=\"HardcodedText\" />\n\n    <EditText\n        android:id=\"@+id/th_server\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_columnWeight=\"4\"\n        android:hint=\"th.biliroaming.example.com\"\n        android:tag=\"th_server\"\n        android:inputType=\"textUri\"\n        android:autofillHints=\"no\"\n        tools:ignore=\"HardcodedText\" />\n</GridLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_argb_color_choose.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"120dp\"\n            android:layout_marginTop=\"14dp\">\n\n            <ImageView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"120dp\"\n                android:scaleType=\"center\"\n                android:src=\"@drawable/tp\"\n                tools:ignore=\"ContentDescription\" />\n\n            <View\n                android:id=\"@+id/view_sample2\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"120dp\" />\n        </RelativeLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"#\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"20sp\" />\n\n            <EditText\n                android:id=\"@+id/et_color2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:digits=\"0123456789abcdefABCDEF\"\n                android:gravity=\"center\"\n                android:minWidth=\"80dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"20sp\"\n                tools:text=\"FF009788\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"A\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorA2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorA2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"0\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"R\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorR2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorR2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"0\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"G\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorG2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorG2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"0\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"B\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorB2\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorB2\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"233\" />\n\n        </LinearLayout>\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_color_choose.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <View\n            android:id=\"@+id/view_sample\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"120dp\"\n            android:layout_marginTop=\"14dp\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"#\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"20sp\" />\n\n            <EditText\n                android:id=\"@+id/et_color\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:digits=\"0123456789abcdefABCDEF\"\n                android:gravity=\"center\"\n                android:minWidth=\"80dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"20sp\"\n                tools:text=\"009788\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"R\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorR\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorR\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"0\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"G\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorG\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorG\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"0\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"10dp\"\n            android:gravity=\"center_vertical\"\n            android:paddingLeft=\"24dp\"\n            android:paddingRight=\"24dp\">\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"B\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\" />\n\n            <SeekBar\n                android:id=\"@+id/sb_colorB\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:max=\"255\" />\n\n            <TextView\n                android:id=\"@+id/tv_colorB\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:minWidth=\"27dp\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"16sp\"\n                tools:text=\"233\" />\n\n        </LinearLayout>\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/feature.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\">\n\n    <TextView\n        android:id=\"@+id/textView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"216dp\"\n        android:layout_margin=\"14dp\"\n        android:text=\"@string/feature_dialog\"\n        android:textAlignment=\"center\"\n        android:textAppearance=\"@android:style/TextAppearance.Material.Large\"\n        android:textColor=\"@android:color/tab_indicator_text\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"horizontal\">\n\n        <ImageView\n            android:id=\"@+id/imageView2\"\n            android:layout_width=\"198dp\"\n            android:layout_height=\"wrap_content\"\n            android:contentDescription=\"@string/demo\"\n            android:src=\"@drawable/demo2\" />\n\n        <ImageView\n            android:id=\"@+id/imageView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:contentDescription=\"@string/demo\"\n            android:src=\"@drawable/demo\" />\n    </LinearLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/search_bar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/contentLayout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"48dp\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\"\n    android:paddingLeft=\"16dp\"\n    android:paddingTop=\"8dp\"\n    android:paddingRight=\"16dp\"\n    android:paddingBottom=\"8dp\">\n\n    <EditText\n        android:id=\"@+id/search\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_weight=\"1\"\n        android:background=\"@null\"\n        android:hint=\"@string/search_hint\"\n        android:imeOptions=\"actionSearch\"\n        android:inputType=\"textNoSuggestions\"\n        android:maxLength=\"20\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n    <ImageView\n        android:id=\"@+id/clear\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?android:attr/selectableItemBackgroundBorderless\"\n        android:padding=\"4dp\"\n        android:src=\"@drawable/ic_clear\"\n        android:tint=\"?android:attr/textColorPrimary\"\n        android:visibility=\"visible\" />\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/seekbar_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\"\n    android:paddingStart=\"16dp\"\n    android:paddingEnd=\"16dp\">\n\n    <SeekBar\n        android:id=\"@+id/seekBar\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:paddingTop=\"16dp\"\n        android:paddingBottom=\"16dp\" />\n\n    <TextView\n        android:id=\"@+id/tvHint\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"end\"\n        android:minWidth=\"32dp\"\n        android:textColor=\"?android:attr/textColorPrimaryNoDisable\"\n        android:textSize=\"14sp\"\n        tools:text=\"1\" />\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/video_choose.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <CheckBox\n        android:id=\"@+id/cb\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\" />\n\n    <TextView\n        android:id=\"@+id/tv_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_toEndOf=\"@id/cb\"\n        android:textSize=\"14sp\" />\n\n    <TextView\n        android:id=\"@+id/tv_pageTitle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/tv_title\"\n        android:layout_alignStart=\"@id/tv_title\"\n        android:textSize=\"13sp\" />\n\n    <TextView\n        android:id=\"@+id/tv_bvid\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/tv_pageTitle\"\n        android:layout_alignStart=\"@id/tv_pageTitle\"\n        android:textSize=\"12sp\" />\n\n    <TextView\n        android:id=\"@+id/tv_aid\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/tv_pageTitle\"\n        android:layout_marginStart=\"5dp\"\n        android:layout_toEndOf=\"@id/tv_bvid\"\n        android:textSize=\"12sp\" />\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>\n"
  },
  {
    "path": "app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"upos_entries\">\n        <item>不替换</item>\n        <item>ali（阿里）</item>\n        <item>alib（阿里）</item>\n        <item>alio1（阿里）</item>\n        <item>bos（百度）</item>\n        <item>cos（腾讯）</item>\n        <item>cosb（腾讯）</item>\n        <item>coso1（腾讯）</item>\n        <item>hw（华为）</item>\n        <item>hwb（华为）</item>\n        <item>hwo1（华为）</item>\n        <item>08c（华为）</item>\n        <item>08h（华为）</item>\n        <item>08ct（华为）</item>\n        <item>tf_hw（华为）</item>\n        <item>tf_tx（腾讯）</item>\n        <item>akamai（Akamai海外）</item>\n        <item>aliov（阿里海外）</item>\n        <item>cosov（腾讯海外）</item>\n        <item>hwov（华为海外）</item>\n        <item>hk_bcache（Bilibili海外）</item>\n    </string-array>\n    <string-array name=\"upos_values\">\n        <item>$1</item>\n        <item>@string/ali_host</item>\n        <item>@string/alib_host</item>\n        <item>@string/alio1_host</item>\n        <item>@string/bos_host</item>\n        <item>@string/cos_host</item>\n        <item>@string/cosb_host</item>\n        <item>@string/coso1_host</item>\n        <item>@string/hw_host</item>\n        <item>@string/hwb_host</item>\n        <item>@string/hwo1_host</item>\n        <item>@string/hw_08c_host</item>\n        <item>@string/hw_08h_host</item>\n        <item>@string/hw_08ct_host</item>\n        <item>@string/tf_hw_host</item>\n        <item>@string/tf_tx_host</item>\n        <item>@string/akamai_host</item>\n        <item>@string/aliov_host</item>\n        <item>@string/cosov_host</item>\n        <item>@string/hwov_host</item>\n        <item>@string/hk_bcache_host</item>\n    </string-array>\n    <string-array name=\"tab_entries\">\n        <item>直播</item>\n        <item>推荐</item>\n        <item>热门</item>\n        <item>番剧（动画）</item>\n        <item>影视</item>\n        <item>韩综</item>\n        <item>其它</item>\n    </string-array>\n    <string-array name=\"tab_values\">\n        <item>live</item>\n        <item>promo</item>\n        <item>hottopic</item>\n        <item>bangumi</item>\n        <item>movie</item>\n        <item>korea</item>\n        <item>other_tabs</item>\n    </string-array>\n    <string-array name=\"home_recommend_entries\">\n        <item>广告（含推广）</item>\n        <item>游戏</item>\n        <item>横幅（轮播图）</item>\n        <item>通知（追番更新、活动提示）</item>\n        <item>文章</item>\n        <item>动态</item>\n        <item>竖屏视频</item>\n        <item>直播</item>\n        <item>内联视频</item>\n        <item>番剧、电影、电视剧、纪录片……</item>\n        <item>大卡（单列显示的）</item>\n        <item>中卡</item>\n        <item>小卡（双列显示的）</item>\n    </string-array>\n    <string-array name=\"home_recommend_values\">\n        <item>advertisement</item>\n        <item>game</item>\n        <item>banner</item>\n        <item>notify</item>\n        <item>article</item>\n        <item>picture</item>\n        <item>vertical</item>\n        <item>live</item>\n        <item>inline</item>\n        <item>bangumi</item>\n        <item>large_cover</item>\n        <item>middle_cover</item>\n        <item>small_cover</item>\n    </string-array>\n    <string-array name=\"space_entries\">\n        <item>分页：主页</item>\n        <item>分页：动态</item>\n        <item>分页：投稿</item>\n        <item>分页：商品</item>\n        <item>分页：追番</item>\n        <item>分页：课堂</item>\n        <item>直播</item>\n        <item>充电</item>\n        <item>大航海</item>\n        <item>推广橱窗</item>\n        <item>视频</item>\n        <item>专栏</item>\n        <item>音频</item>\n        <item>最近追番</item>\n        <item>最近投币</item>\n        <item>最近点赞</item>\n        <item>最近追漫</item>\n        <item>玩的游戏</item>\n        <item>课堂</item>\n        <item>粉丝装扮</item>\n        <item>收藏夹</item>\n        <item>漫画</item>\n        <item>合集</item>\n        <item>数字藏品展现</item>\n        <item>NFT头像</item>\n    </string-array>\n    <string-array name=\"space_values\">\n        <item>tab.home</item>\n        <item>tab.dynamic</item>\n        <item>tab.contribute</item>\n        <item>tab.shop</item>\n        <item>tab.bangumi</item>\n        <item>tab.cheese</item>\n        <item>liveEntry</item>\n        <item>chargeResult</item>\n        <item>guard</item>\n        <item>adV2</item>\n        <item>archiveVideo</item>\n        <item>article</item>\n        <item>audio</item>\n        <item>season</item>\n        <item>coinVideo</item>\n        <item>recommendVideo</item>\n        <item>followComicList</item>\n        <item>spaceGame</item>\n        <item>cheeseVideo</item>\n        <item>fansDress</item>\n        <item>favoriteBox</item>\n        <item>comicList</item>\n        <item>ugcSeasonList</item>\n        <item>contractResource</item>\n        <item>nftShowModule</item>\n    </string-array>\n\n    <string-array name=\"block_video_ad\">\n        <item>UP主推荐广告</item>\n        <item>相关游戏</item>\n    </string-array>\n    <string-array name=\"block_video_ad_values\">\n        <item>up_recommend</item>\n        <item>game</item>\n    </string-array>\n\n    <string-array name=\"xposed_scope\">\n        <item>tv.danmaku.bili</item>\n        <item>com.bilibili.app.blue</item>\n        <item>com.bilibili.app.in</item>\n        <item>tv.danmaku.bilibilihd</item>\n    </string-array>\n\n    <string-array name=\"dynamic_entries\">\n        <item>转发</item>\n        <item>投稿视频</item>\n        <item>番剧、电影等</item>\n        <item>付费内容 1</item>\n        <item>付费内容 2</item>\n        <item>折叠</item>\n        <item>文字</item>\n        <item>图文</item>\n        <item>文章</item>\n        <item>音频</item>\n        <item>通用 方形</item>\n        <item>通用 竖形</item>\n        <item>直播</item>\n        <item>播放列表</item>\n        <item>广告</item>\n        <item>小程序</item>\n        <item>订阅</item>\n        <item>新订阅</item>\n        <item>直播推荐</item>\n        <item>横幅</item>\n        <item>合集</item>\n        <item>故事</item>\n        <item>话题推荐</item>\n        <item>付费更新</item>\n        <item>话题集合</item>\n        <item>通知</item>\n    </string-array>\n    <string-array name=\"dynamic_values\">\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n        <item>4</item>\n        <item>14</item>\n        <item>5</item>\n        <item>6</item>\n        <item>7</item>\n        <item>8</item>\n        <item>9</item>\n        <item>10</item>\n        <item>11</item>\n        <item>12</item>\n        <item>13</item>\n        <item>15</item>\n        <item>16</item>\n        <item>17</item>\n        <item>21</item>\n        <item>18</item>\n        <item>19</item>\n        <item>20</item>\n        <item>22</item>\n        <item>23</item>\n        <item>24</item>\n        <item>25</item>\n        <item>26</item>\n    </string-array>\n    <string-array name=\"live_popup_entries\">\n        <item>购物卡片</item>\n        <item>购物精选</item>\n        <item>关注提醒</item>\n        <item>直播预约</item>\n        <item>投喂支持</item>\n        <item>滚动横幅</item>\n        <item>电池任务</item>\n        <item>正在去买</item>\n    </string-array>\n    <string-array name=\"live_popup_values\">\n        <item>shoppingCard</item>\n        <item>shoppingSelected</item>\n        <item>follow</item>\n        <item>reserve</item>\n        <item>gift</item>\n        <item>banner</item>\n        <item>task</item>\n        <item>gotoBuy</item>\n    </string-array>\n    <string-array name=\"half_screen_quality_entries\">\n        <item>默认</item>\n        <item>跟随全屏清晰度</item>\n        <item>240P</item>\n        <item>360P</item>\n        <item>480P</item>\n        <item>720P</item>\n        <item>720P 60帧</item>\n        <item>1080P</item>\n        <item>1080P 高码率</item>\n        <item>1080P 60帧</item>\n        <item>4K</item>\n        <item>8K</item>\n    </string-array>\n    <string-array name=\"half_screen_quality_values\">\n        <item>0</item>\n        <item>1</item>\n        <item>6</item>\n        <item>16</item>\n        <item>32</item>\n        <item>64</item>\n        <item>74</item>\n        <item>80</item>\n        <item>112</item>\n        <item>116</item>\n        <item>120</item>\n        <item>127</item>\n    </string-array>\n    <string-array name=\"live_quality_entries\">\n        <item>默认</item>\n        <item>流畅</item>\n        <item>高清</item>\n        <item>超清</item>\n        <item>蓝光</item>\n        <item>原画</item>\n        <item>4K</item>\n        <item>杜比</item>\n    </string-array>\n    <string-array name=\"live_quality_values\">\n        <item>0</item>\n        <item>80</item>\n        <item>150</item>\n        <item>250</item>\n        <item>400</item>\n        <item>10000</item>\n        <item>20000</item>\n        <item>30000</item>\n    </string-array>\n    <string-array name=\"full_screen_quality_entries\">\n        <item>默认</item>\n        <item>240P</item>\n        <item>360P</item>\n        <item>480P</item>\n        <item>720P</item>\n        <item>720P 60帧</item>\n        <item>1080P</item>\n        <item>1080P 高码率</item>\n        <item>1080P 60帧</item>\n        <item>4K</item>\n        <item>8K</item>\n    </string-array>\n    <string-array name=\"full_screen_quality_values\">\n        <item>0</item>\n        <item>6</item>\n        <item>16</item>\n        <item>32</item>\n        <item>64</item>\n        <item>74</item>\n        <item>80</item>\n        <item>112</item>\n        <item>116</item>\n        <item>120</item>\n        <item>127</item>\n    </string-array>\n    <string-array name=\"pegasus_cover_ratio_entries\">\n        <item>默认（使用云控值）</item>\n        <item>16:9</item>\n        <item>4:3</item>\n    </string-array>\n    <string-array name=\"pegasus_cover_ratio_values\">\n        <item>0</item>\n        <item>1.777778</item>\n        <item>1.333333</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#fff</color>\n    <color name=\"text_search_hint\">#fa6496</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"app_name\">哔哩漫游</string>\n    <string name=\"xposed_description\">解锁B站港澳台番剧限制</string>\n    <string name=\"about\">关于</string>\n    <string name=\"version\">版本</string>\n    <string name=\"author\">作者</string>\n    <string name=\"running_status_enable\">已启用</string>\n    <string name=\"running_status_disable\">未启用</string>\n    <string name=\"not_running_summary\">哔哩漫游未激活</string>\n    <string name=\"runtime_xposed\">运行环境：Xposed</string>\n    <string name=\"runtime_taichi\">运行环境：太极</string>\n    <string name=\"main_func_title\">解锁番剧限制</string>\n    <string name=\"main_func_summary\">使用户可以观看区域外版权番剧</string>\n    <string name=\"hide_icon_title\">隐藏桌面图标</string>\n    <string name=\"hide_icon_summary\">不显示本应用在启动器中的图标</string>\n    <string name=\"custom_theme_title\">自定义主题色</string>\n    <string name=\"custom_theme_summary\">添加自定义主题颜色的功能（需从我的右上角调色盘图标进入）</string>\n    <string name=\"teenagers_mode_dialog_title\">关闭青少年模式弹窗</string>\n    <string name=\"teenagers_mode_dialog_summary\">仍可从侧滑菜单进入青少年模式</string>\n    <string name=\"allow_download_title\">允许下载版权番剧</string>\n    <string name=\"allow_download_summary\">使版权番剧可以缓存</string>\n    <string name=\"cdn_category\">加速 CDN 设置</string>\n    <string name=\"hidden_title\">启用隐藏功能</string>\n    <string name=\"hidden_summary\">打开后可以使用隐藏功能</string>\n    <string name=\"purify_game_title\">净化游戏中心</string>\n    <string name=\"purify_game_summary\">去除首页右上角游戏中心</string>\n    <string name=\"hidden_group\">隐藏功能</string>\n    <string name=\"show_info_summary\">显示解锁版权、允许下载等状态信息</string>\n    <string name=\"show_info_title\">提示模块运行信息</string>\n    <string name=\"main_category\">解锁番剧限制</string>\n    <string name=\"miscellaneous_category\">杂项</string>\n    <string name=\"setting_category\">设置</string>\n    <string name=\"mini_program_title\">去你大爷的小程序</string>\n    <string name=\"mini_program_summary\">不再以小程序的方式分享到QQ或者微信（同时复制分享链接变为av号）</string>\n    <string name=\"update_title\">更新</string>\n    <string name=\"update_summary\">点击打开最新版下载地址</string>\n    <string name=\"group_title\">QQ频道</string>\n    <string name=\"group_summary\">点击加入QQ频道</string>\n    <string name=\"tg_title\">TG频道</string>\n    <string name=\"tg_summary\">点击加入TG频道</string>\n    <string name=\"custom_upos_title\">UPOS 服务器设置</string>\n    <string name=\"custom_upos_summary\">自定义番剧的 UPOS 服务器，可解决黑屏与无限加载</string>\n    <string name=\"purify_splash_title\">净化开屏页面</string>\n    <string name=\"purify_splash_summary\">去除启动客户端时的推广</string>\n    <string name=\"custom_splash_title\">替换启动图</string>\n    <string name=\"custom_splash_summary\">替换启动图为自定义图片（仅支持6.6.0及以上版本，不选择图片则隐藏）</string>\n    <string name=\"custom_splash_logo_title\">替换启动Logo</string>\n    <string name=\"custom_splash_logo_summary\">替换启动Logo为自定义图片（仅支持6.6.0及以上版本，不选择图片则隐藏，延迟生效）</string>\n    <string name=\"beautify_category\">美化</string>\n    <string name=\"auto_like_title\">自动点赞视频</string>\n    <string name=\"auto_like_summary\">进入视频详情页时自动点赞</string>\n    <string name=\"feature_title\">功能设置</string>\n    <string name=\"feature_summary\">打开哔哩哔哩客户端，进入【我的】页面，点击【哔哩漫游设置】；旧版本打开侧边栏，长按【设置】</string>\n    <string name=\"feature_dialog\">当哔哩漫游被正确激活后，打开哔哩哔哩客户端，进入【我的】页面，就能看到【哔哩漫游设置】；使用旧版本客户端则打开侧边栏，长按【设置】。如果不知道如何激活插件，请直接使用Xpatch版。</string>\n    <string name=\"help_title\">帮助</string>\n    <string name=\"help_summary\">点击查看激活方法、常见问题等</string>\n    <string name=\"purify_search_title\">净化搜索页面</string>\n    <string name=\"purify_search_summary\">去除搜索页面的热搜和推荐</string>\n    <string name=\"demo\">示例</string>\n    <string name=\"not_support\">当前版本客户端不支持此功能</string>\n    <string name=\"os_not_support\">当前系统版本不支持此功能</string>\n    <string name=\"enable_av_title\">显示AV号</string>\n    <string name=\"enable_av_summary\">视频详情页显示为AV号</string>\n    <string name=\"setting_title\">打开模块设置</string>\n    <string name=\"setting_summary\">启动客户端并打开模块设置</string>\n    <string name=\"custom_download_thread_title\">自定义同时缓存数</string>\n    <string name=\"custom_download_thread_summary\">自定义最多可以同时下载的视频个数（超过一个需要大会员）</string>\n    <string name=\"save_log_title\">保存日志到文件</string>\n    <string name=\"save_log_summary\">启用后，将保存日志到文件%s</string>\n    <string name=\"fix_download_title\">解除版权番剧下载限速</string>\n    <string name=\"fix_download_summary\">提高版权番剧的下载速度，但是会导致音视频分离</string>\n    <string name=\"customize_home_tab_title\">净化首页标签</string>\n    <string name=\"customize_home_tab_summary\">请勿全部选择，会使用之前的的标签</string>\n    <string name=\"music_notification_title\">原生音乐通知样式</string>\n    <string name=\"music_notification_summary\">强制音乐通知栏为原生样式</string>\n    <string name=\"custom_server_title\">设置解析服务器</string>\n    <string name=\"custom_server_summary\">使用设置的服务器解析区域限制番剧的播放地址；如不知如何设置，请查看帮助文档</string>\n    <string name=\"drawer_title\">移动我的页面到侧边栏</string>\n    <string name=\"drawer_summary\">移动后，可以点击首页的头像或者在空白地方左划打开侧边栏\\n关闭时，需要重启两次客户端才能复原。如果无法还原，请重启手机。</string>\n    <string name=\"full_splash_title\">强制满屏启动图</string>\n    <string name=\"full_splash_summary\">去两头留中间形式满屏，无论是否自定启动图</string>\n    <!--    <string name=\"get_cover_title\">长按保存封面</string>-->\n    <!--    <string name=\"get_cover_summary\">开启后，长按播放器可保存视频、直播间、番剧封面</string>-->\n    <string name=\"purify_city_title\">移除同城标签</string>\n    <string name=\"purify_campus_title\">移除校园标签</string>\n    <string name=\"setting\">设置</string>\n    <string name=\"speed\">速度</string>\n    <string name=\"test_upos_title\">UPOS 服务器测速</string>\n    <string name=\"test_upos_summary\">选择最快的 UPOS 服务器以获得最好体验</string>\n    <string name=\"search_area_title\">开启其它地区番剧搜索</string>\n    <string name=\"search_area_summary\">在搜索页面添加其它地区番剧搜索结果，需要解析服务器支持</string>\n    <string name=\"customize_bottom_bar_title\">自定义底栏</string>\n    <string name=\"customize_bottom_bar_summary\">至少保留一个元素（重启两次生效）</string>\n    <string name=\"customize_accessKey_title\">自定义访问密钥</string>\n    <string name=\"customize_accessKey_summary\">自定义发往解析服务器的访问密钥（调试功能，除非明白自己在做什么，否则保持空白）</string>\n    <string name=\"export_video_title\">导出视频</string>\n    <string name=\"export_video_summary\">只支持默认下载位置</string>\n    <string name=\"customize_home_recommend_title\">净化首页推荐内容</string>\n    <string name=\"customize_home_recommend_summary\">去除首页推荐页面的指定内容（某些旧版可能无效）</string>\n    <string name=\"pref_backup\">备份和还原</string>\n    <string name=\"pref_export_title\">导出配置</string>\n    <string name=\"pref_import_title\">导入配置</string>\n    <string name=\"hide_low_play_count_recommend_title\">隐藏低播放量视频</string>\n    <string name=\"hide_low_play_count_recommend_summary\">隐藏低于指定播放量的视频</string>\n    <string name=\"hide_top_entrance_popular_summary\">隐藏热门的顶栏(排行榜等)</string>\n    <string name=\"hide_topic_list_popular_summary\">隐藏热门的话题</string>\n    <string name=\"hide_suggest_follow_popular_summary\">隐藏热门的推荐关注(及其视频)</string>\n    <string name=\"add_bangumi_title\">添加其他地区番剧</string>\n    <string name=\"add_bangumi_summary\">在首页标签添加大陆/港澳台番剧分页</string>\n    <string name=\"add_movie_title\">添加其他地区影视</string>\n    <string name=\"add_movie_summary\">在首页标签添加大陆/港澳台影视分页</string>\n    <string name=\"custom_subtitle_title\">自定义字幕样式</string>\n    <string name=\"custom_subtitle_summary\">开启后能自定义CC字幕样式，但透明度跟随弹幕透明度</string>\n    <string name=\"customize_space_title\">净化用户空间</string>\n    <string name=\"customize_space_summary\">选择要屏蔽的元素（全选会让你什么也看不到）</string>\n    <string name=\"comment_copy_summary\">禁用长按评论/动态/视频简介复制到剪切板（减少误触）</string>\n    <string name=\"comment_copy_title\">去除长按复制</string>\n    <string name=\"comment_copy_enhance_title\">长按自由复制</string>\n    <string name=\"comment_copy_enhance_summary\">长按私信/评论/动态/视频简介时弹出自由选择对话框，需要开启“去除长按复制”</string>\n    <string name=\"copy_comment_title\">评论自由复制</string>\n    <string name=\"copy_comment_summary\">评论复制按钮可以自由复制</string>\n    <string name=\"revert_live_room_feed_title\">强制旧版直播间样式</string>\n    <string name=\"revert_live_room_feed_summary\">强制直播间使用旧版样式</string>\n    <string name=\"block_video_ad_title\">屏蔽视频下方推荐</string>\n    <string name=\"block_video_ad_summary\">屏蔽视频下方推荐栏，开启后会同时屏蔽评论区顶部推广横幅</string>\n    <string name=\"hide_long_duration_recommend_title\">隐藏视频时长大于</string>\n    <string name=\"hide_short_duration_recommend_title\">隐藏视频时长小于</string>\n    <string name=\"hide_duration_recommend_summary\">隐藏指定时长的视频（秒）</string>\n    <string name=\"home_filter_title\">首页过滤器(推荐和热门)</string>\n    <string name=\"home_filter_summary\">过滤首页推荐和热门的内容（重启两次生效）</string>\n    <string name=\"keywords_filter_recommend_summary\">过滤包含指定关键词的内容</string>\n    <string name=\"search_area_movie_title\">开启其它地区影剧搜索</string>\n    <string name=\"search_area_movie_summary\">在搜索页面添加其它地区影剧搜索结果，需要解析服务器支持</string>\n    <string name=\"force_th_comment_title\">强制开启泰区评论弹幕</string>\n    <string name=\"force_th_comment_summary\">强制开启串台的泰区评论弹幕（仅调试使用）</string>\n    <string name=\"share_log_title\">分享日志</string>\n    <string name=\"share_log_summary\">可能在旧版无效</string>\n    <string name=\"replace_story_video_title\">替换竖版视频</string>\n    <string name=\"replace_story_video_summary\">替换竖版视频为正常视频</string>\n    <string name=\"customize_drawer_title\">自定义我的页面</string>\n    <string name=\"customize_drawer_summary\">自定义【我的】页面（重启后生效）</string>\n    <string name=\"purify_drawer_reddot_title\">净化我的页面红点</string>\n    <string name=\"purify_drawer_reddot_summary\">净化【我的】页面下方项目红点</string>\n    <string name=\"drawer_style_switch_title\">修改我的页面样式开关</string>\n    <string name=\"drawer_style_switch_summary\">修改【我的】页面样式</string>\n    <string name=\"drawer_style_title\">修改我的页面样式</string>\n    <string name=\"drawer_style_summary\">开启改为列表，关闭改为按钮</string>\n    <string name=\"purify_share_title\">净化分享</string>\n    <string name=\"purify_share_summary\">把分享链接中的短链接换成正常链接</string>\n    <string name=\"custom_link_title\">开启自定义链接</string>\n    <string name=\"custom_link_summary\">仅支持 bilibili:// 开头的格式</string>\n    <string name=\"add_custom_button_title\">在【我的】页面添加自定义按钮</string>\n    <string name=\"add_custom_button_summary\">指定修改其中一个项目</string>\n    <string name=\"add_korea_title\">添加其他地区韩综</string>\n    <string name=\"add_korea_summary\">在首页标签添加港澳台韩综分页</string>\n    <string name=\"fix_space_title\">修复UP主空间</string>\n    <string name=\"fix_space_summary\">修复部分被屏蔽的UP主空间</string>\n    <string name=\"play_arc_conf_title\">解除播放限制</string>\n    <string name=\"play_arc_conf_summary\">解除部分版权视频禁止后台播放、小窗播放、投屏、听视频等限制</string>\n    <string name=\"customize_dynamic_title\">净化动态</string>\n    <string name=\"customize_dynamic_summary\">支持移除同城及校园标签，支持移除话题及最常访问，支持按类型和关键词过滤动态</string>\n    <string name=\"customize_dynamic_prefer_video_tab\">首选视频标签</string>\n    <string name=\"customize_dynamic_all_rm_topic_title\">移除热门话题（综合）</string>\n    <string name=\"customize_dynamic_all_rm_up_title\">移除最常访问UP（综合）</string>\n    <string name=\"customize_dynamic_all_rm_live_title\">移除最常访问直播（综合）</string>\n    <string name=\"customize_dynamic_video_rm_up_title\">移除最常访问（视频）</string>\n    <string name=\"customize_dynamic_filter_apply_to_video\">过滤设置同时作用于视频分区</string>\n    <string name=\"customize_dynamic_rm_blocked_title\">屏蔽无权查看动态(例如充电专属)</string>\n    <string name=\"customize_dynamic_rm_ad_link_title\">屏蔽跳转广告动态(例如“UP主的推荐”)</string>\n    <string name=\"customize_dynamic_by_type\">按类型</string>\n    <string name=\"customize_dynamic_by_keyword\">按关键词</string>\n    <string name=\"keyword_group_name_content\">内容</string>\n    <string name=\"keyword_group_name_up\">UP主</string>\n    <string name=\"keyword_group_name_title\">标题</string>\n    <string name=\"keyword_group_name_rcmd_reason\">推荐原因</string>\n    <string name=\"keyword_group_name_category\">分区(仅推荐)</string>\n    <string name=\"keyword_group_name_channel\">频道(仅推荐)</string>\n    <string name=\"keyword_group_add\">添加</string>\n    <string name=\"keyword_group_delete\">删除</string>\n    <string name=\"keyword_group_clear\">清空</string>\n    <string name=\"prefs_save_success_and_reboot\">保存成功 重启后生效</string>\n    <string name=\"remove_video_relate_promote_title\">移除视频下方的相关推广</string>\n    <string name=\"remove_video_relate_promote_summary\">移除视频下方的相关推广，例如游戏、课程、会员购、创作推广等</string>\n    <string name=\"remove_video_relate_only_av_title\">仅展示UP主投稿视频</string>\n    <string name=\"remove_video_relate_only_av_summary\">上个选项的基础上，仅展示UP主投稿视频，移除番剧等内容</string>\n    <string name=\"remove_video_honor_title\">移除视频下方的获得荣誉</string>\n    <string name=\"remove_video_honor_summary\">移除视频下方的荣誉信息，例如入站必刷、每周必看、全站排行榜、4K频道精选等</string>\n    <string name=\"remove_video_UgcSeason_title\">移除视频下方的分集列表</string>\n    <string name=\"remove_video_UgcSeason_summary\">移除视频下方的UP主视频分集列表</string>\n    <string name=\"remove_video_relate_nothing_title\">移除视频下方的所有内容</string>\n    <string name=\"remove_video_relate_nothing_summary\">上个选项的基础上，将UP主投稿视频也移除</string>\n    <string name=\"remove_video_cmd_dms_title\">移除视频浮窗</string>\n    <string name=\"remove_video_cmd_dms_summary\">移除视频播放时出现的浮窗，包括三连关注、关联视频、投票弹幕、评分弹幕、投资当前UP主、跳转其他应用、免流引导等</string>\n    <string name=\"block_word_search_title\">屏蔽评论关键词搜索功能</string>\n    <string name=\"block_word_search_summary\">屏蔽视频评论蓝色关键词点击触发跳转搜索功能</string>\n    <string name=\"disable_try_watch_vip_quality_title\">禁用免费试看大会员清晰度</string>\n    <string name=\"disable_try_watch_vip_quality_desc\">鼠鼠我呀，真的不想每30天点一次不再试看呢</string>\n    <string name=\"allow_mini_play_title\">允许版权限制番剧小窗播放</string>\n    <string name=\"allow_mini_play_summary\">港澳台番剧的字幕并不能在小窗显示</string>\n    <string name=\"force_browser_title\">在外部浏览器打开链接</string>\n    <string name=\"force_browser_summary\">而不是使用内置浏览器</string>\n    <string name=\"disable_story_full_title\">关闭看一看按钮</string>\n    <string name=\"disable_story_full_summary\">横屏稿件半屏时不显示看一看按钮</string>\n    <string name=\"disable_main_page_story_title\">点击首页头像跳转“我的”页面</string>\n    <string name=\"disable_main_page_story_summary\">开启后点击首页头像跳转至“我的”页面而不是快捷刷短视频。若启用“移动我的页面到侧边栏”则本项无效。可能在旧版无效。</string>\n    <string name=\"dialog_blur_background_summary\">花里胡哨! (需要 Android 12+ 并开启 允许窗口级模糊处理)</string>\n    <string name=\"dialog_blur_background_title\">全局 Dialog 背景模糊</string>\n    <string name=\"block_up_rcmd_ads_title\">屏蔽UP主推荐广告</string>\n    <string name=\"block_up_rcmd_ads_summary\">屏蔽视频播放过程中，浮窗出现的UP主推荐广告</string>\n    <string name=\"block_view_page_ads_title\">屏蔽视频详情页面广告</string>\n    <string name=\"block_view_page_ads_summary\">屏蔽视频（含番剧，普通视频等）详情页面的广告，包括但不限于弹窗广告，会员购等活动广告（旧版本仅对番剧详情页面生效）。</string>\n    <string name=\"block_live_order_title\">屏蔽直播预约</string>\n    <string name=\"block_live_order_summary\">屏蔽视频详情页面的直播预约信息</string>\n    <string name=\"remove_up_vip_label_title\">移除 UP 大会员标识</string>\n    <string name=\"remove_up_vip_label_summary\">移除视频详情页 UP 名字旁出现的的大会员标识</string>\n    <string name=\"generate_subtitle_title\">生成简中字幕</string>\n    <string name=\"generate_subtitle_summary\">根据繁体字幕自动生成简体中文字幕</string>\n    <string name=\"forbid_switch_live_room_title\">禁止滑动切换直播间</string>\n    <string name=\"forbid_switch_live_room_summary\">禁止上下滑动切换直播间</string>\n    <string name=\"forbid_player_long_click_accelerate_title\">禁用播放器长按加速</string>\n    <string name=\"forbid_player_long_click_accelerate_summary\">禁用播放器长按开启倍速播放的功能</string>\n    <string name=\"custom_subtitle_remove_bg\">移除字幕背景</string>\n    <string name=\"custom_subtitle_bold\">粗体显示</string>\n    <string name=\"custom_subtitle_fill_color\">字体颜色</string>\n    <string name=\"custom_subtitle_stroke_color\">描边颜色</string>\n    <string name=\"custom_subtitle_stroke_width\">描边宽度</string>\n    <string name=\"custom_subtitle_bg_color\">背景颜色</string>\n    <string name=\"custom_subtitle_font_size\">字体大小</string>\n    <string name=\"custom_subtitle_font_size_portrait\">字体大小 - 竖屏</string>\n    <string name=\"custom_subtitle_font_size_landscape\">字体大小 - 横屏</string>\n    <string name=\"custom_subtitle_blur\">字体阴影</string>\n    <string name=\"custom_subtitle_test\">测试内容</string>\n    <string name=\"custom_subtitle_test_text\">字幕样式预览</string>\n    <string name=\"custom_subtitle_fix_break\">修复换行 (除非想解决字幕换行问题, 否则不要开启)</string>\n    <string name=\"custom_subtitle_preview\">预览</string>\n    <string name=\"custom_subtitle_pick_color\">取色</string>\n    <string name=\"custom_subtitle_font\">字体</string>\n    <string name=\"custom_subtitle_reset\">重置</string>\n    <string name=\"custom_subtitle_import\">导入</string>\n    <string name=\"custom_subtitle_status_default\">当前：默认</string>\n    <string name=\"custom_subtitle_status_custom\">当前：自定义</string>\n    <string name=\"custom_subtitle_zero_as_default\">0表示默认</string>\n    <string name=\"custom_subtitle_offset\">字幕偏移</string>\n    <string name=\"custom_subtitle_offset_hint\">0表示默认，正上负下</string>\n    <string name=\"regex_mode\">正则模式</string>\n    <string name=\"invalid_regex\">保存失败，正则表达式有误</string>\n    <string name=\"purify_live_popups_title\">净化直播间浮窗</string>\n    <string name=\"purify_live_popups_summary\">净化直播间各种浮窗</string>\n    <string name=\"disable_live_room_double_click_title\">禁用直播间双击点赞</string>\n    <string name=\"disable_live_room_double_click_summary\">将直播间双击点赞行为更换为暂停/播放</string>\n    <string name=\"block_modules_title\">禁止下载 App 基础组件库</string>\n    <string name=\"block_modules_summary\">在清理存储空间中清理 App 基础组件库后不再下载，能节省存储空间，但会影响部分功能。</string>\n    <string name=\"block_update_title\">禁止 App 检查更新</string>\n    <string name=\"remove_vip_section_title\">移除我的大会员横幅</string>\n    <string name=\"remove_vip_section_summary\">我的页面将大会员横幅移除</string>\n    <string name=\"save_comment_image_title\">笔记图片长按预览保存</string>\n    <string name=\"save_comment_image_summary\">视频笔记图片点开后长按预览图片保存到本地</string>\n    <string name=\"force_upos_title\">应用 UPOS 服务器到所有视频</string>\n    <string name=\"force_upos_summary\">【非必要不开启】含普通视频、番剧、电影等。特殊网络环境视频加载缓慢，可尝试开启</string>\n    <string name=\"block_pcdn_title\">阻止视频通过 PCDN 加载</string>\n    <string name=\"block_pcdn_summary\">【非必要不开启】可一定程度上优化视频加载速度，以实际情况为准</string>\n    <string name=\"block_pcdn_live_title\">阻止直播通过 PCDN 加载</string>\n    <string name=\"block_pcdn_live_summary\">【非必要不开启】避免占用大量上传带宽，同时可一定程度上优化直播加载速度，以实际情况为准</string>\n    <string name=\"danmaku_filter_title\">弹幕屏蔽等级</string>\n    <string name=\"danmaku_filter_summary\">恢复旧版按等级屏蔽弹幕功能，0级表示关闭本功能</string>\n    <string name=\"danmaku_filter_weight_hint\">%1$d级</string>\n    <string name=\"half_screen_quality_title\">视频半屏清晰度</string>\n    <string name=\"half_screen_quality_summary\">设置视频半屏播放时的清晰度，设置后会大幅降低视频的首次播放加载速度，旧版播放器（半屏时进度条在框内）仅支持跟随全屏清晰度，需开启解锁番剧限制选项</string>\n    <string name=\"full_screen_quality_title\">视频全屏清晰度</string>\n    <string name=\"full_screen_quality_summary\">设置视频全屏播放时默认的清晰度，当播放器画质选项为自动时此选项不生效，需开启解锁番剧限制选项</string>\n    <string name=\"live_quality_title\">直播清晰度</string>\n    <string name=\"live_quality_summary\">设置进入直播间的默认清晰度，若不存在指定清晰度，则会取最接近的清晰度</string>\n    <string name=\"block_comment_guide_title\">屏蔽评论引导</string>\n    <string name=\"block_comment_guide_summary\">屏蔽视频详情页及评论页的评论引导提示</string>\n    <string name=\"disable_auto_refresh_title\">禁止首页自动刷新</string>\n    <string name=\"disable_auto_refresh_summary\">禁止首页内容在退至后台、切换其他页面一段时间后返回或热重启时自动刷新</string>\n    <!--\n    <string name=\"disable_auto_select_title\">禁止自动转到动态</string>\n    <string name=\"disable_auto_select_summary\">禁止评论带图时自动勾选转到动态</string>\n    -->\n    <string name=\"apply_to_relate_title\">同时应用于视频下方推荐</string>\n    <string name=\"disable_vip_dm_colorful_title\">过滤渐变色弹幕</string>\n    <string name=\"disable_vip_dm_colorful_summary\">渐变色（非普通彩色）弹幕显示为白色</string>\n    <string name=\"search_hint\" tools:ignore=\"TypographyEllipsis\">搜索...</string>\n    <string name=\"block_video_comment_title\">屏蔽视频评论区</string>\n    <string name=\"support\">捐赠</string>\n    <string name=\"support_summary\">捐赠以支持模块开发</string>\n    <string name=\"default_speed_summary\">设置视频播放默认速度，视频播放后可手动覆盖该速度</string>\n    <string name=\"default_speed_title\">视频默认播放速度</string>\n    <string name=\"filter_search_title\">过滤搜索</string>\n    <string name=\"filter_search_summary\">按内容过滤搜索结果</string>\n    <string name=\"filter_search_video\">过滤视频</string>\n    <string name=\"filter_search_remove_relate_promote\">搜索结果移除推广</string>\n    <string name=\"filter_comment_title\">过滤评论</string>\n    <string name=\"filter_comment_summary\">按内容过滤评论</string>\n    <string name=\"keyword_group_name_at_up\">\\@的用户名</string>\n    <string name=\"keyword_group_name_at_uid\">\\@的uid</string>\n    <string name=\"comment_filter_block_at_comment_title\">屏蔽只含\\@的评论</string>\n    <string name=\"target_comment_author_level_title\">屏蔽指定等级以下用户评论</string>\n    <string name=\"pegasus_cover_ratio_title\">推荐封面比例</string>\n    <string name=\"pegasus_cover_ratio_summary\">自定义首页推荐小卡（双列显示的）封面比例</string>\n    <string name=\"fake_non_multiwindow_title\">伪装处于非多窗口模式</string>\n    <string name=\"fake_non_multiwindow_summary\">比如视频多窗口模式下全屏</string>\n    <string name=\"copy_access_key_title\">复制 access_key</string>\n    <string name=\"copy_access_key_summary\">请勿泄露给他人</string>\n    <string name=\"copy_access_key_toast\">已复制</string>\n    <string name=\"auto_dark_splash_title\">启动屏背景适配深色</string>\n    <string name=\"auto_dark_splash_summary\">防瞎眼</string>\n    <string name=\"no_live_mask_title\">隐藏直播模糊遮罩</string>\n    <string name=\"no_live_mask_summary\">隐藏部分直播的模糊遮罩</string>\n    <string name=\"skip_reward_ad_title\">跳过视频激励广告</string>\n    <string name=\"skip_reward_ad_summary\">有了这个，终于能过第二关了😋（需重启两次哔哩哔哩）</string>\n    <string name=\"purify_story_video_ad_title\">净化竖屏模式广告/推广</string>\n    <string name=\"purify_story_video_ad_summary\">有了这个，连第一关都看不到了😭</string>\n    <string name=\"long_press_speed\">长按播放速度设置</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings_raw.xml",
    "content": "<!-- Here place some not translatable string resources -->\n<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"MissingTranslation\">\n    <string name=\"author_list\">yujincheng08/iAcn/djytw/</string>\n    <string name=\"version_url\">https://api.github.com/repos/yujincheng08/BiliRoaming/releases/latest</string>\n    <string name=\"update_url\">https://github.com/yujincheng08/BiliRoaming/releases/latest</string>\n    <string name=\"server_url\">https://github.com/yujincheng08/BiliRoaming/wiki/%E5%85%AC%E5%85%B1%E8%A7%A3%E6%9E%90%E6%9C%8D%E5%8A%A1%E5%99%A8</string>\n    <string name=\"github_url\">https://github.com/yujincheng08/BiliRoaming</string>\n    <string name=\"afdian_url\">https://afdian.net/a/yujincheng08</string>\n    <string name=\"tg_url\">https://t.me/biliroaming</string>\n    <string name=\"help_url\">https://github.com/yujincheng08/BiliRoaming/wiki</string>\n    <string name=\"group_url\">mqqguild://guild/share?inviteCode=NVoD5&amp;from=246610</string>\n    <string name=\"bos_host\">upos-sz-mirrorbos.bilivideo.com</string>\n    <string name=\"cos_host\">upos-sz-mirrorcos.bilivideo.com</string>\n    <string name=\"cosb_host\">upos-sz-mirrorcosb.bilivideo.com</string>\n    <string name=\"coso1_host\">upos-sz-mirrorcoso1.bilivideo.com</string>\n    <string name=\"hw_host\">upos-sz-mirrorhw.bilivideo.com</string>\n    <string name=\"hwb_host\">upos-sz-mirrorhwb.bilivideo.com</string>\n    <string name=\"hwo1_host\">upos-sz-mirrorhwo1.bilivideo.com</string>\n    <string name=\"hw_08c_host\">upos-sz-mirror08c.bilivideo.com</string>\n    <string name=\"hw_08h_host\">upos-sz-mirror08h.bilivideo.com</string>\n    <string name=\"hw_08ct_host\">upos-sz-mirror08ct.bilivideo.com</string>\n    <string name=\"ali_host\">upos-sz-mirrorali.bilivideo.com</string>\n    <string name=\"alib_host\">upos-sz-mirroralib.bilivideo.com</string>\n    <string name=\"alio1_host\">upos-sz-mirroralio1.bilivideo.com</string>\n    <string name=\"akamai_host\">upos-hz-mirrorakam.akamaized.net</string>\n    <string name=\"aliov_host\">upos-sz-mirroraliov.bilivideo.com</string>\n    <string name=\"hwov_host\">upos-sz-mirrorhwov.bilivideo.com</string>\n    <string name=\"cosov_host\">upos-sz-mirrorcosov.bilivideo.com</string>\n    <string name=\"hk_bcache_host\">cn-hk-eq-bcache-01.bilivideo.com</string>\n    <string name=\"tf_hw_host\">upos-tf-all-hw.bilivideo.com</string>\n    <string name=\"tf_tx_host\">upos-tf-all-tx.bilivideo.com</string>\n    <string name=\"speed_formatter\">%sKB/s</string>\n    <string name=\"upos\">UPOS</string>\n    <string name=\"keyword_group_name_uid\">UID</string>\n    <string name=\"subtitle_convert_failed\">字幕转换失败，请重试</string>\n    <string name=\"subtitle_dict_download_failed\">转换字典下载失败，请重试</string>\n    <string name=\"subtitle_append_info\">请注意，站内宣传漫游或脚本会被拉黑</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <style name=\"MainTheme\" parent=\"android:Theme.Material.Light.NoActionBar\">\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"p\">true</item>\n        <item name=\"android:colorAccent\">#1a73e9</item>\n        <item name=\"android:fitsSystemWindows\">true</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#303030</color>\n    <color name=\"text_search_hint\">#e66e90</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <style name=\"MainTheme\" parent=\"android:Theme.Material.NoActionBar\">\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">false</item>\n        <item name=\"android:windowLightNavigationBar\" tools:targetApi=\"p\">false</item>\n        <item name=\"android:colorAccent\">#64b5f6</item>\n        <item name=\"android:fitsSystemWindows\">true</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"upos_entries\">\n        <item>不取代</item>\n        <item>ali（阿里）</item>\n        <item>alib（阿里）</item>\n        <item>alio1（阿里）</item>\n        <item>bos（百度）</item>\n        <item>cos（騰訊）</item>\n        <item>cosb（騰訊）</item>\n        <item>coso1（騰訊）</item>\n        <item>hw（華為）</item>\n        <item>hwb（華為）</item>\n        <item>hwo1（華為）</item>\n        <item>08c（華為）</item>\n        <item>08h（華為）</item>\n        <item>08ct（華為）</item>\n        <item>tf_hw（華為）</item>\n        <item>tf_tx（騰訊）</item>\n        <item>akamai（Akamai海外）</item>\n        <item>aliov（阿里海外）</item>\n        <item>cosov（騰訊海外）</item>\n        <item>hwov（華為海外）</item>\n        <item>hk_bcache（Bilibili海外）</item>\n    </string-array>\n\n    <string-array name=\"tab_entries\">\n        <item>直播</item>\n        <item>推薦</item>\n        <item>熱門</item>\n        <item>番劇（動畫）</item>\n        <item>影視</item>\n        <item>韓綜</item>\n        <item>其它</item>\n    </string-array>\n\n    <string-array name=\"home_recommend_entries\">\n        <item>廣告（含推廣）</item>\n        <item>遊戲</item>\n        <item>橫幅（輪播圖）</item>\n        <item>通知（追番更新、活動提示）</item>\n        <item>文章</item>\n        <item>動態</item>\n        <item>豎版影片</item>\n        <item>直播</item>\n        <item>內聯影片</item>\n        <item>番劇、電影、電視劇、紀錄片……</item>\n        <item>大卡（單列顯示的）</item>\n        <item>中卡</item>\n        <item>小卡（雙列顯示的）</item>\n    </string-array>\n\n    <string-array name=\"space_entries\">\n        <item>分頁：首頁</item>\n        <item>分頁：動態</item>\n        <item>分頁：投稿</item>\n        <item>分頁：商品</item>\n        <item>分頁：追番</item>\n        <item>分頁：課堂</item>\n        <item>直播</item>\n        <item>充電</item>\n        <item>大航海</item>\n        <item>推廣櫥窗</item>\n        <item>影片</item>\n        <item>專欄</item>\n        <item>音訊</item>\n        <item>最近追番</item>\n        <item>最近投幣</item>\n        <item>最近按讚</item>\n        <item>最近追漫</item>\n        <item>玩的遊戲</item>\n        <item>課堂</item>\n        <item>粉絲裝扮</item>\n        <item>收藏夾</item>\n        <item>漫畫</item>\n        <item>合集</item>\n        <item>數字藏品展現</item>\n        <item>NFT大頭貼</item>\n    </string-array>\n\n    <string-array name=\"block_video_ad\">\n        <item>UP主推薦廣告</item>\n        <item>相關遊戲</item>\n    </string-array>\n\n    <string-array name=\"dynamic_entries\">\n        <item>轉發</item>\n        <item>投稿影片</item>\n        <item>番劇、電影等</item>\n        <item>付費內容 1</item>\n        <item>付費內容 2</item>\n        <item>摺疊</item>\n        <item>文字</item>\n        <item>圖文</item>\n        <item>文章</item>\n        <item>音訊</item>\n        <item>通用 方形</item>\n        <item>通用 豎形</item>\n        <item>直播</item>\n        <item>播放列表</item>\n        <item>廣告</item>\n        <item>小程式</item>\n        <item>訂閱</item>\n        <item>新訂閱</item>\n        <item>直播推薦</item>\n        <item>橫幅</item>\n        <item>合集</item>\n        <item>故事</item>\n        <item>話題推薦</item>\n        <item>付費更新</item>\n        <item>話題集合</item>\n        <item>通知</item>\n    </string-array>\n    <string-array name=\"live_popup_entries\">\n        <item>購物卡片</item>\n        <item>購物精選</item>\n        <item>關注提醒</item>\n        <item>直播預約</item>\n        <item>投餵支持</item>\n        <item>滾動橫幅</item>\n        <item>電池任務</item>\n        <item>正在去買</item>\n    </string-array>\n    <string-array name=\"half_screen_quality_entries\">\n        <item>預設</item>\n        <item>跟隨全螢幕解析度</item>\n        <item>240P</item>\n        <item>360P</item>\n        <item>480P</item>\n        <item>720P</item>\n        <item>720P 60幀</item>\n        <item>1080P</item>\n        <item>1080P 高碼率</item>\n        <item>1080P 60幀</item>\n        <item>4K</item>\n        <item>8K</item>\n    </string-array>\n    <string-array name=\"full_screen_quality_entries\">\n        <item>預設</item>\n        <item>240P</item>\n        <item>360P</item>\n        <item>480P</item>\n        <item>720P</item>\n        <item>720P 60幀</item>\n        <item>1080P</item>\n        <item>1080P 高碼率</item>\n        <item>1080P 60幀</item>\n        <item>4K</item>\n        <item>8K</item>\n    </string-array>\n    <string-array name=\"pegasus_cover_ratio_entries\">\n        <item>預設（使用雲控值）</item>\n        <item>16:9</item>\n        <item>4:3</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"app_name\">嗶哩漫遊</string>\n    <string name=\"xposed_description\">解鎖B站港澳台番劇限制</string>\n    <string name=\"about\">關於</string>\n    <string name=\"version\">版本</string>\n    <string name=\"author\">作者</string>\n    <string name=\"running_status_enable\">已啟用</string>\n    <string name=\"running_status_disable\">未啟用</string>\n    <string name=\"not_running_summary\">嗶哩漫遊未啟動</string>\n    <string name=\"runtime_xposed\">執行環境：Xposed</string>\n    <string name=\"runtime_taichi\">執行環境：太極</string>\n    <string name=\"main_func_title\">解鎖番劇限制</string>\n    <string name=\"main_func_summary\">使用戶可以觀看區域外版權番劇</string>\n    <string name=\"hide_icon_title\">隱藏桌面圖示</string>\n    <string name=\"hide_icon_summary\">不顯示本程式在啟動器中的圖示</string>\n    <string name=\"custom_theme_title\">自訂主題色</string>\n    <string name=\"custom_theme_summary\">新增自訂主題顏色的功能（需從我的右上角調色盤圖示進入）</string>\n    <string name=\"teenagers_mode_dialog_title\">關閉青少年模式跳出視窗</string>\n    <string name=\"teenagers_mode_dialog_summary\">仍可從側滑選單進入青少年模式</string>\n    <string name=\"allow_download_title\">允許下載版權番劇</string>\n    <string name=\"allow_download_summary\">使版權番劇可以快取</string>\n    <string name=\"cdn_category\">加速 CDN 設定</string>\n    <string name=\"hidden_title\">啟用隱藏功能</string>\n    <string name=\"hidden_summary\">打開後可以使用隱藏功能</string>\n    <string name=\"purify_game_title\">淨化遊戲中心</string>\n    <string name=\"purify_game_summary\">去除首頁右上角遊戲中心</string>\n    <string name=\"hidden_group\">隱藏功能</string>\n    <string name=\"show_info_summary\">顯示解鎖版權、允許下載等狀態訊息</string>\n    <string name=\"show_info_title\">提示模組執行訊息</string>\n    <string name=\"main_category\">解鎖番劇限制</string>\n    <string name=\"miscellaneous_category\">雜項</string>\n    <string name=\"setting_category\">設定</string>\n    <string name=\"mini_program_title\">去你大爺的小程式</string>\n    <string name=\"mini_program_summary\">不再以小程式的方式分享到QQ或者微信（同時複製分享連結變為av號）</string>\n    <string name=\"update_title\">更新</string>\n    <string name=\"update_summary\">點擊打開最新版下載網址</string>\n    <string name=\"group_title\">QQ頻道</string>\n    <string name=\"group_summary\">點擊加入QQ頻道</string>\n    <string name=\"tg_title\">TG頻道</string>\n    <string name=\"tg_summary\">點擊加入TG頻道</string>\n    <string name=\"custom_upos_title\">UPOS 伺服器設定</string>\n    <string name=\"custom_upos_summary\">自訂番劇的 UPOS 伺服器，可以解決黑畫面與無限載入</string>\n    <string name=\"purify_splash_title\">淨化啟動頁面</string>\n    <string name=\"purify_splash_summary\">去除啟動程式時的推廣</string>\n    <string name=\"custom_splash_title\">取代啟動圖</string>\n    <string name=\"custom_splash_summary\">取代啟動圖為自訂圖片（僅支援6.6.0及以上版本，不選擇圖片則隱藏）</string>\n    <string name=\"custom_splash_logo_title\">取代啟動Logo</string>\n    <string name=\"custom_splash_logo_summary\">取代啟動Logo為自訂圖片（僅支援6.6.0及以上版本，不選擇圖片則隱藏，延遲生效）</string>\n    <string name=\"beautify_category\">美化</string>\n    <string name=\"auto_like_title\">自動按讚影片</string>\n    <string name=\"auto_like_summary\">進入影片詳情頁時自動按讚</string>\n    <string name=\"feature_title\">功能設定</string>\n    <string name=\"feature_summary\">打開嗶哩嗶哩，進入【我的】頁面，點擊【嗶哩漫遊設定】；舊版本打開側邊欄，長按【設定】</string>\n    <string name=\"feature_dialog\">當嗶哩漫遊被正確啟動後，打開嗶哩嗶哩，進入【我的】頁面，就能看到【嗶哩漫遊設定】；使用舊版本則打開側邊欄，長按【設定】。如果不知道如何啟動模組，請直接使用Xpatch版。</string>\n    <string name=\"help_title\">幫助</string>\n    <string name=\"help_summary\">點擊查看啟動方法、常見問題等</string>\n    <string name=\"purify_search_title\">淨化搜尋頁面</string>\n    <string name=\"purify_search_summary\">去除搜尋頁面的熱搜和推薦</string>\n    <string name=\"demo\">範例</string>\n    <string name=\"not_support\">目前版本用戶端不支援此功能</string>\n    <string name=\"os_not_support\">目前系統版本不支援此功能</string>\n    <string name=\"enable_av_title\">顯示AV號</string>\n    <string name=\"enable_av_summary\">影片詳情頁顯示為AV號</string>\n    <string name=\"setting_title\">打開模組設定</string>\n    <string name=\"setting_summary\">啟動用戶端並打開模組設定</string>\n    <string name=\"custom_download_thread_title\">自訂同時快取數</string>\n    <string name=\"custom_download_thread_summary\">自訂最多可以同時下載的影片個數（超過一個需要大會員）</string>\n    <string name=\"save_log_title\">儲存日誌到文件</string>\n    <string name=\"save_log_summary\">啟用後，將儲存日誌到文件%s</string>\n    <string name=\"fix_download_title\">解除版權番劇下載限速</string>\n    <string name=\"fix_download_summary\">提高版權番劇的下載速度，但是會導致影音分離</string>\n    <string name=\"customize_home_tab_title\">淨化首頁標籤</string>\n    <string name=\"customize_home_tab_summary\">請勿全部選擇，會使用之前的的標籤</string>\n    <string name=\"custom_server_title\">設定解析伺服器</string>\n    <string name=\"custom_server_summary\">使用設定的伺服器解析區域限制番劇的播放地址；如不知如何設定，請查看說明文件</string>\n    <string name=\"drawer_title\">移動我的頁面到側邊欄</string>\n    <string name=\"drawer_summary\">移動後，可以點擊首頁的大頭貼或者在空白地方左劃打開側邊欄\\n關閉時，需要重啟兩次用戶端才能還原。如果無法還原，請重啟手機。</string>\n    <string name=\"full_splash_title\">強制全螢幕啟動圖</string>\n    <string name=\"full_splash_summary\">去兩頭留中間形式全螢幕，無論是否自訂啟動圖</string>\n    <!--    <string name=\"get_cover_title\">長按儲存封面</string>-->\n    <!--    <string name=\"get_cover_summary\">開啟後，長按播放器可儲存影片、直播間、番劇封面</string>-->\n    <string name=\"purify_city_title\">移除同城標籤</string>\n    <string name=\"purify_campus_title\">移除校園標籤</string>\n    <string name=\"setting\">設定</string>\n    <string name=\"speed\">速度</string>\n    <string name=\"test_upos_title\">UPOS 伺服器測速</string>\n    <string name=\"test_upos_summary\">選擇最快的 UPOS 伺服器以獲得最好體驗</string>\n    <string name=\"search_area_title\">開啟其它地區番劇搜尋</string>\n    <string name=\"search_area_summary\">在搜尋頁面新增其它地區番劇搜尋結果，需要解析伺服器支援</string>\n    <string name=\"customize_bottom_bar_title\">自訂底欄</string>\n    <string name=\"customize_bottom_bar_summary\">至少保留一個元素（重啟兩次生效）</string>\n    <string name=\"customize_accessKey_title\">自訂存取金鑰</string>\n    <string name=\"customize_accessKey_summary\">自訂發往解析伺服器的存取金鑰（除錯功能，除非明白自己在做什麼，否則保持空白）</string>\n    <string name=\"export_video_title\">匯出影片</string>\n    <string name=\"export_video_summary\">只支援預設下載位置</string>\n    <string name=\"customize_home_recommend_title\">淨化首頁推薦內容</string>\n    <string name=\"customize_home_recommend_summary\">去除首頁推薦頁面的指定內容（某些舊版可能無效）</string>\n    <string name=\"pref_backup\">備份和還原</string>\n    <string name=\"pref_export_title\">匯出配置</string>\n    <string name=\"pref_import_title\">匯入配置</string>\n    <string name=\"hide_low_play_count_recommend_title\">隱藏低播放量影片</string>\n    <string name=\"hide_low_play_count_recommend_summary\">隱藏低於指定播放量的影片</string>\n    <string name=\"hide_top_entrance_popular_summary\">隱藏熱門的頂欄（排行榜等）</string>\n    <string name=\"hide_topic_list_popular_summary\">隱藏熱門的話題</string>\n    <string name=\"hide_suggest_follow_popular_summary\">隱藏熱門的推薦關注（及其影片）</string>\n    <string name=\"add_bangumi_title\">新增其他地區番劇</string>\n    <string name=\"add_bangumi_summary\">在首頁標籤新增大陸/港澳台番劇分頁</string>\n    <string name=\"add_movie_title\">新增其他地區影視</string>\n    <string name=\"add_movie_summary\">在首頁標籤新增大陸/港澳台影視分頁</string>\n    <string name=\"custom_subtitle_title\">自訂字幕樣式</string>\n    <string name=\"custom_subtitle_summary\">開啟後能自訂CC字幕樣式，但透明度跟隨彈幕透明度</string>\n    <string name=\"customize_space_title\">淨化使用者空間</string>\n    <string name=\"customize_space_summary\">選擇要封鎖的項目（全選會讓你什麼也看不到）</string>\n    <string name=\"comment_copy_summary\">禁用長按評論/動態/影片簡介複製到剪貼簿（減少誤觸）</string>\n    <string name=\"comment_copy_title\">去除長按複製</string>\n    <string name=\"comment_copy_enhance_title\">長按自由複製</string>\n    <string name=\"comment_copy_enhance_summary\">長按私信/評論/動態/影片簡介時彈出自由選擇對話框，需要開啟「去除長按複製」</string>\n    <string name=\"copy_comment_title\">评论自由复制</string>\n    <string name=\"copy_comment_summary\">评论复制按钮可以自由复制</string>\n    <string name=\"revert_live_room_feed_title\">強制舊版直播間樣式</string>\n    <string name=\"revert_live_room_feed_summary\">強制直播間使用舊版樣式</string>\n    <string name=\"block_video_ad_title\">封鎖影片下方推薦</string>\n    <string name=\"block_video_ad_summary\">封鎖影片下方推薦欄，開啟後會同時封鎖評論區頂部推廣橫幅</string>\n    <string name=\"hide_long_duration_recommend_title\">隱藏影片時長大於</string>\n    <string name=\"hide_short_duration_recommend_title\">隱藏影片時長小於</string>\n    <string name=\"hide_duration_recommend_summary\">隱藏指定時長的影片（秒）</string>\n    <string name=\"home_filter_title\">首頁過濾器（推薦和熱門）</string>\n    <string name=\"home_filter_summary\">過濾首頁推薦和熱門推送的內容（重啓兩次生效）</string>\n    <string name=\"keywords_filter_recommend_summary\">過濾包含指定關鍵字的內容</string>\n    <string name=\"search_area_movie_title\">開啟其它地區影劇搜尋</string>\n    <string name=\"search_area_movie_summary\">在搜尋頁面新增其它地區影劇搜尋結果，需要解析伺服器支援</string>\n    <string name=\"force_th_comment_title\">強制開啟泰區評論彈幕</string>\n    <string name=\"force_th_comment_summary\">強制開啟串台的泰區評論彈幕（僅除錯使用）</string>\n    <string name=\"share_log_title\">分享日誌</string>\n    <string name=\"share_log_summary\">可能在舊版無效</string>\n    <string name=\"replace_story_video_title\">取代豎版影片</string>\n    <string name=\"replace_story_video_summary\">取代豎版影片為正常影片</string>\n    <string name=\"customize_drawer_title\">自訂我的頁面</string>\n    <string name=\"customize_drawer_summary\">自訂【我的】頁面（重啟後生效）</string>\n    <string name=\"purify_drawer_reddot_title\">淨化我的頁面紅點</string>\n    <string name=\"purify_drawer_reddot_summary\">淨化【我的】頁面下方項目紅點</string>\n    <string name=\"drawer_style_switch_title\">修改我的頁面樣式開關</string>\n    <string name=\"drawer_style_switch_summary\">修改【我的】頁面樣式</string>\n    <string name=\"drawer_style_title\">修改我的頁面樣式</string>\n    <string name=\"drawer_style_summary\">開啟改為列表，關閉改為按鈕</string>\n    <string name=\"purify_share_title\">淨化分享</string>\n    <string name=\"purify_share_summary\">把分享連結中的短連結換成正常連結</string>\n    <string name=\"custom_link_title\">開啟自訂連結</string>\n    <string name=\"custom_link_summary\">僅支援 bilibili:// 開頭的格式</string>\n    <string name=\"add_custom_button_title\">在【我的】頁面新增自訂按鈕</string>\n    <string name=\"add_custom_button_summary\">指定修改其中一個項目</string>\n    <string name=\"add_korea_title\">新增其他地區韓綜</string>\n    <string name=\"add_korea_summary\">在首頁標籤新增港澳台韓綜分頁</string>\n    <string name=\"fix_space_title\">修復UP主空間</string>\n    <string name=\"fix_space_summary\">修復部份被封鎖的UP主空間</string>\n    <string name=\"play_arc_conf_title\">解除播放限制</string>\n    <string name=\"play_arc_conf_summary\">解除部分版權影片禁止後台播放、小窗播放、投屏、聽影片等限制</string>\n    <string name=\"customize_dynamic_title\">淨化動態</string>\n    <string name=\"customize_dynamic_summary\">支援移除同城及校園標籤，支援移除話題及最常訪問，支援按類型和關鍵字過濾動態</string>\n    <string name=\"customize_dynamic_prefer_video_tab\">首選影片標籤</string>\n    <string name=\"customize_dynamic_all_rm_topic_title\">移除熱門話題（綜合）</string>\n    <string name=\"customize_dynamic_all_rm_up_title\">移除最常訪問（綜合）</string>\n    <string name=\"customize_dynamic_video_rm_up_title\">移除最常訪問（影片）</string>\n    <string name=\"customize_dynamic_filter_apply_to_video\">過濾設定同時作用於影片分區</string>\n    <string name=\"customize_dynamic_rm_blocked_title\">封鎖無權查看動態(例如充電專屬)</string>\n    <string name=\"customize_dynamic_rm_ad_link_title\">封鎖跳轉廣告動態(例如“UP主的推薦”)</string>\n    <string name=\"customize_dynamic_by_type\">按類型</string>\n    <string name=\"customize_dynamic_by_keyword\">按關鍵字</string>\n    <string name=\"keyword_group_name_content\">內容</string>\n    <string name=\"keyword_group_name_up\">UP主</string>\n    <string name=\"keyword_group_name_title\">標題</string>\n    <string name=\"keyword_group_name_rcmd_reason\">推薦原因</string>\n    <string name=\"keyword_group_name_category\">分區（僅推薦）</string>\n    <string name=\"keyword_group_name_channel\">頻道（僅推薦）</string>\n    <string name=\"keyword_group_add\">新增</string>\n    <string name=\"keyword_group_delete\">刪除</string>\n    <string name=\"keyword_group_clear\">清空</string>\n    <string name=\"prefs_save_success_and_reboot\">儲存成功 重啟後生效</string>\n    <string name=\"remove_video_relate_promote_title\">移除影片下方的相關推廣</string>\n    <string name=\"remove_video_relate_promote_summary\">移除影片下方的相關推廣，例如遊戲、課程、會員購、創作推廣等</string>\n    <string name=\"remove_video_relate_only_av_title\">僅展示UP主投稿影片</string>\n    <string name=\"remove_video_relate_only_av_summary\">上個選項的基礎上，僅展示UP主投稿影片，移除番劇等內容</string>\n    <string name=\"remove_video_honor_title\">移除影片下方的獲得榮譽</string>\n    <string name=\"remove_video_honor_summary\">移除影片下方的榮譽訊息，例如入站必刷、每週必看、全站排行榜、4K頻道精選等</string>\n    <string name=\"remove_video_UgcSeason_title\">移除影片下方的分集列表</string>\n    <string name=\"remove_video_UgcSeason_summary\">移除影片下方的UP主影片分集列表</string>\n    <string name=\"remove_video_relate_nothing_title\">移除影片下方的所有內容</string>\n    <string name=\"remove_video_relate_nothing_summary\">上個選項的基礎上，將UP主投稿影片也移除</string>\n    <string name=\"remove_video_cmd_dms_title\">移除影片浮窗</string>\n    <string name=\"remove_video_cmd_dms_summary\">移除影片播放時出現的浮窗，包括三連關注、關聯影片、投票彈幕、評分彈幕、投資目前UP主、跳轉應用程式、免流引導等</string>\n    <string name=\"block_word_search_title\">封鎖評論關鍵字搜尋功能</string>\n    <string name=\"block_word_search_summary\">封鎖影片評論藍色關鍵字點擊觸發跳轉搜尋功能</string>\n    <string name=\"disable_try_watch_vip_quality_title\">禁用免費試看大會員解析度</string>\n    <string name=\"disable_try_watch_vip_quality_desc\">鼠鼠我呀，真的不想每30天點一次不再試看呢</string>\n    <string name=\"allow_mini_play_title\">允許版權限制番劇小窗播放</string>\n    <string name=\"allow_mini_play_summary\">港澳台番劇的字幕並不能在小窗顯示</string>\n    <string name=\"force_browser_title\">在外部瀏覽器打開連結</string>\n    <string name=\"force_browser_summary\">而不是使用內建瀏覽器</string>\n    <string name=\"disable_story_full_title\">關閉看一看按鈕</string>\n    <string name=\"disable_story_full_summary\">橫向稿件在稿件詳情頁時不顯示看一看按鈕</string>\n    <string name=\"disable_main_page_story_title\">點擊首頁頭像跳到「我的」頁面</string>\n    <string name=\"disable_main_page_story_summary\">開啟後，點擊首頁頭像跳到「我的」頁面，而非快捷切換到短影片頁面。如果啟用「移動我的頁面到側邊欄」，則本項設定無效。可能無法在舊版中使用。</string>\n    <string name=\"dialog_blur_background_summary\">花裡胡哨! (需要 Android 12+ 並開啟 允許視窗級模糊處理)</string>\n    <string name=\"dialog_blur_background_title\">全域 Dialog 背景模糊</string>\n    <string name=\"block_up_rcmd_ads_title\">封鎖UP主推薦廣告</string>\n    <string name=\"block_up_rcmd_ads_summary\">封鎖影片播放過程中，浮窗出現的UP主推薦廣告</string>\n    <string name=\"block_view_page_ads_title\">封鎖影片詳情頁面廣告</string>\n    <string name=\"block_view_page_ads_summary\">封鎖影片（包含番劇、普通影片等）詳情頁面的廣告，包括但不限於彈窗廣告、會員購等活動廣告（舊版本僅對番劇頁面生效）。</string>\n    <string name=\"block_live_order_title\">封鎖直播預約</string>\n    <string name=\"block_live_order_summary\">封鎖影片詳情頁的直播預約信息</string>\n    <string name=\"remove_up_vip_label_title\">移除 UP 大會員標識</string>\n    <string name=\"remove_up_vip_label_summary\">移除影片詳情頁 UP 名字旁出現的的大會員標識</string>\n    <string name=\"generate_subtitle_title\">生成簡中字幕</string>\n    <string name=\"generate_subtitle_summary\">根據繁體字幕自動生成簡體中文字幕</string>\n    <string name=\"forbid_switch_live_room_title\">禁止滑動切換直播間</string>\n    <string name=\"forbid_switch_live_room_summary\">禁止上下滑動切換直播間</string>\n    <string name=\"forbid_player_long_click_accelerate_title\">禁用播放器長按加速</string>\n    <string name=\"forbid_player_long_click_accelerate_summary\">禁用播放器長按開啟倍速播放的功能</string>\n    <string name=\"custom_subtitle_remove_bg\">移除字幕背景</string>\n    <string name=\"custom_subtitle_bold\">粗體顯示</string>\n    <string name=\"custom_subtitle_fill_color\">字體顏色</string>\n    <string name=\"custom_subtitle_stroke_color\">描邊顏色</string>\n    <string name=\"custom_subtitle_stroke_width\">描邊寬度</string>\n    <string name=\"custom_subtitle_bg_color\">背景顏色</string>\n    <string name=\"custom_subtitle_font_size\">字體大小</string>\n    <string name=\"custom_subtitle_font_size_portrait\">字體大小 - 豎屏</string>\n    <string name=\"custom_subtitle_font_size_landscape\">字體大小 - 橫屏</string>\n    <string name=\"custom_subtitle_blur\">字體陰影</string>\n    <string name=\"custom_subtitle_test\">測試內容</string>\n    <string name=\"custom_subtitle_test_text\">字幕樣式預覽</string>\n    <string name=\"custom_subtitle_fix_break\">修復換行 (除非想解決字幕換行問題, 否則不要開啟)</string>\n    <string name=\"custom_subtitle_preview\">預覽</string>\n    <string name=\"custom_subtitle_pick_color\">取色</string>\n    <string name=\"custom_subtitle_font\">字體</string>\n    <string name=\"custom_subtitle_reset\">重設</string>\n    <string name=\"custom_subtitle_import\">匯入</string>\n    <string name=\"custom_subtitle_status_default\">目前：預設</string>\n    <string name=\"custom_subtitle_status_custom\">目前：自訂</string>\n    <string name=\"custom_subtitle_zero_as_default\">0表示預設</string>\n    <string name=\"custom_subtitle_offset\">字幕偏移</string>\n    <string name=\"custom_subtitle_offset_hint\">0表示預設，正上負下</string>\n    <string name=\"regex_mode\">正則模式</string>\n    <string name=\"invalid_regex\">儲存失敗，正則表達式有誤</string>\n    <string name=\"purify_live_popups_title\">淨化直播間浮窗</string>\n    <string name=\"purify_live_popups_summary\">淨化直播間各種浮窗</string>\n    <string name=\"disable_live_room_double_click_title\">禁用直播間雙擊按讚</string>\n    <string name=\"disable_live_room_double_click_summary\">將直播間雙擊按讚行為更換為暫停/播放</string>\n    <string name=\"block_modules_title\">禁止下載 App 基礎組件庫</string>\n    <string name=\"block_modules_summary\">在清理儲存空間中清理 App 基礎組件庫後不再下載，能節省儲存空間，但會影響部分功能。</string>\n    <string name=\"block_update_title\">禁止 App 檢查更新</string>\n    <string name=\"remove_vip_section_title\">移除我的大會員橫幅</string>\n    <string name=\"remove_vip_section_summary\">我的頁面將大會員橫幅移除</string>\n    <string name=\"save_comment_image_title\">筆記圖片長按預覽儲存</string>\n    <string name=\"save_comment_image_summary\">影片筆記圖片點開後長按預覽圖片儲存到本地</string>\n    <string name=\"force_upos_title\">套用 UPOS 伺服器至所有影片</string>\n    <string name=\"force_upos_summary\">【非必要不開啟】含一般影片、番劇、電影等。在特殊網路環境下影片載入慢，可嘗試開啟</string>\n    <string name=\"block_pcdn_title\">阻止影片使用 PCDN 載入</string>\n    <string name=\"block_pcdn_summary\">【非必要不開啟】可以在一定程度上最佳化影片載入速度，以實際情況為準</string>\n    <string name=\"block_pcdn_live_title\">阻止直播使用 PCDN 載入</string>\n    <string name=\"block_pcdn_live_summary\">【非必要不開啟】避免佔用大量上傳頻寬，同時可以在一定程度上最佳化直播載入速度，但以實際情況為準</string>\n    <string name=\"danmaku_filter_title\">彈幕封鎖等級</string>\n    <string name=\"danmaku_filter_summary\">恢復舊版按等級封鎖彈幕功能，0級表示關閉本功能</string>\n    <string name=\"danmaku_filter_weight_hint\">%1$d級</string>\n    <string name=\"half_screen_quality_title\">影片半屏解析度</string>\n    <string name=\"half_screen_quality_summary\">設定影片半屏播放時的解析度，設定後會大幅降低影片的首次播放載入速度，舊版播放器（半屏時進度條在框內）僅支援跟隨全螢幕解析度，需開啟解鎖番劇限制選項</string>\n    <string name=\"full_screen_quality_title\">影片全螢幕解析度</string>\n    <string name=\"full_screen_quality_summary\">設定影片全螢幕播放時預設的解析度，當播放器畫質選項為自動時此選項不生效，需開啟解鎖番劇限制選項</string>\n    <string name=\"block_comment_guide_title\">封鎖評論引導</string>\n    <string name=\"block_comment_guide_summary\">封鎖影片詳情頁及評論頁的評論引導提示</string>\n    <string name=\"disable_auto_refresh_title\">禁止首頁自動重新整理</string>\n    <string name=\"disable_auto_refresh_summary\">禁止首頁內容在退至後台、切換其他頁面一段時間後返回或熱重啟時自動重新整理</string>\n    <!--\n    <string name=\"disable_auto_select_title\">禁止自動轉到動態</string>\n    <string name=\"disable_auto_select_summary\">禁止評論帶圖時自動勾選轉到動態</string>\n    -->\n    <string name=\"apply_to_relate_title\">同時套用於影片下方推薦</string>\n    <string name=\"disable_vip_dm_colorful_title\">篩選漸層顏色弹幕</string>\n    <string name=\"disable_vip_dm_colorful_summary\">漸層顏色（非一般彩色）彈幕顯示為白色</string>\n    <string name=\"search_hint\" tools:ignore=\"TypographyEllipsis\">搜尋...</string>\n    <string name=\"block_video_comment_title\">封鎖影片評論區</string>\n    <string name=\"support\">捐贈</string>\n    <string name=\"support_summary\">捐贈以支持模組開發</string>\n    <string name=\"pegasus_cover_ratio_title\">推薦封面比例</string>\n    <string name=\"pegasus_cover_ratio_summary\">自訂首頁推薦小卡（雙列顯示的）封面比例</string>\n    <string name=\"filter_search_remove_relate_promote\">搜尋結果移除推廣</string>\n    <string name=\"skip_reward_ad_title\">跳過視頻激勵廣告</string>\n    <string name=\"skip_reward_ad_summary\">有了這個，終於能過第二關了😋（需重啟兩次嗶哩嗶哩）</string>\n    <string name=\"purify_story_video_ad_title\">淨化豎屏模式廣告/推廣</string>\n    <string name=\"purify_story_video_ad_summary\">有了這個，連第一關都看不到了😭</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/main_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--suppress DeprecatedClassUsageInspection -->\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <PreferenceCategory android:title=\"@string/setting\">\n\n        <Preference\n            android:key=\"feature\"\n            android:summary=\"@string/feature_summary\"\n            android:title=\"@string/feature_title\" />\n\n        <Preference\n            android:key=\"setting\"\n            android:summary=\"@string/setting_summary\"\n            android:title=\"@string/setting_title\" />\n\n        <SwitchPreference\n            android:key=\"hide_icon\"\n            android:summary=\"@string/hide_icon_summary\"\n            android:title=\"@string/hide_icon_title\" />\n\n        <Preference\n            android:key=\"help\"\n            android:summary=\"@string/help_summary\"\n            android:title=\"@string/help_title\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/help_url\" />\n        </Preference>\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:key=\"about\"\n        android:title=\"@string/about\">\n\n        <Preference\n            android:key=\"version\"\n            android:title=\"@string/version\" />\n\n        <Preference\n            android:key=\"author\"\n            android:summary=\"@string/author_list\"\n            android:title=\"@string/author\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/github_url\" />\n        </Preference>\n\n\n        <Preference\n            android:key=\"group\"\n            android:summary=\"@string/group_summary\"\n            android:title=\"@string/group_title\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/group_url\" />\n        </Preference>\n\n        <Preference\n            android:key=\"tg\"\n            android:summary=\"@string/tg_summary\"\n            android:title=\"@string/tg_title\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/tg_url\" />\n        </Preference>\n\n        <Preference android:key=\"running_status\" />\n\n    </PreferenceCategory>\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/prefs_setting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--suppress DeprecatedClassUsageInspection -->\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <PreferenceCategory android:title=\"@string/main_category\">\n        <SwitchPreference\n            android:key=\"main_func\"\n            android:summary=\"@string/main_func_summary\"\n            android:title=\"@string/main_func_title\" />\n\n        <Preference\n            android:dependency=\"main_func\"\n            android:key=\"custom_server\"\n            android:summary=\"@string/custom_server_summary\"\n            android:title=\"@string/custom_server_title\" />\n\n        <SwitchPreference\n            android:dependency=\"main_func\"\n            android:key=\"allow_download\"\n            android:summary=\"@string/allow_download_summary\"\n            android:title=\"@string/allow_download_title\" />\n\n        <SwitchPreference\n            android:dependency=\"allow_download\"\n            android:key=\"fix_download\"\n            android:summary=\"@string/fix_download_summary\"\n            android:title=\"@string/fix_download_title\" />\n\n        <SwitchPreference\n            android:dependency=\"main_func\"\n            android:key=\"allow_mini_play\"\n            android:summary=\"@string/allow_mini_play_summary\"\n            android:title=\"@string/allow_mini_play_title\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory android:title=\"@string/cdn_category\">\n        <ListPreference\n            android:defaultValue=\"@string/ali_host\"\n            android:entries=\"@array/upos_entries\"\n            android:entryValues=\"@array/upos_values\"\n            android:key=\"upos_host\"\n            android:summary=\"@string/custom_upos_summary\"\n            android:title=\"@string/custom_upos_title\" />\n\n        <Preference\n            android:key=\"test_upos\"\n            android:summary=\"@string/test_upos_summary\"\n            android:title=\"@string/test_upos_title\" />\n\n        <SwitchPreference\n            android:key=\"force_upos\"\n            android:summary=\"@string/force_upos_summary\"\n            android:title=\"@string/force_upos_title\" />\n\n        <SwitchPreference\n            android:key=\"block_pcdn\"\n            android:summary=\"@string/block_pcdn_summary\"\n            android:title=\"@string/block_pcdn_title\" />\n\n        <SwitchPreference\n            android:key=\"block_pcdn_live\"\n            android:summary=\"@string/block_pcdn_live_summary\"\n            android:title=\"@string/block_pcdn_live_title\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory android:title=\"@string/beautify_category\">\n        <SwitchPreference\n            android:key=\"custom_theme\"\n            android:summary=\"@string/custom_theme_summary\"\n            android:title=\"@string/custom_theme_title\" />\n        <SwitchPreference\n            android:key=\"auto_dark_splash\"\n            android:summary=\"@string/auto_dark_splash_summary\"\n            android:title=\"@string/auto_dark_splash_title\" />\n        <SwitchPreference\n            android:key=\"custom_splash\"\n            android:summary=\"@string/custom_splash_summary\"\n            android:title=\"@string/custom_splash_title\" />\n        <SwitchPreference\n            android:key=\"custom_splash_logo\"\n            android:summary=\"@string/custom_splash_logo_summary\"\n            android:title=\"@string/custom_splash_logo_title\" />\n        <SwitchPreference\n            android:key=\"full_splash\"\n            android:summary=\"@string/full_splash_summary\"\n            android:title=\"@string/full_splash_title\" />\n        <SwitchPreference\n            android:key=\"music_notification\"\n            android:summary=\"@string/music_notification_summary\"\n            android:title=\"@string/music_notification_title\" />\n        <SwitchPreference\n            android:key=\"drawer\"\n            android:summary=\"@string/drawer_summary\"\n            android:title=\"@string/drawer_title\" />\n        <SwitchPreference\n            android:key=\"custom_subtitle\"\n            android:summary=\"@string/custom_subtitle_summary\"\n            android:title=\"@string/custom_subtitle_title\" />\n        <SwitchPreference\n            android:key=\"disable_story_full\"\n            android:summary=\"@string/disable_story_full_summary\"\n            android:title=\"@string/disable_story_full_title\" />\n        <SwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"disable_main_page_story\"\n            android:summary=\"@string/disable_main_page_story_summary\"\n            android:title=\"@string/disable_main_page_story_title\" />\n        <SwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"dialog_blur_background\"\n            android:summary=\"@string/dialog_blur_background_summary\"\n            android:title=\"@string/dialog_blur_background_title\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory android:title=\"@string/miscellaneous_category\">\n        <SwitchPreference\n            android:key=\"teenagers_mode_dialog\"\n            android:summary=\"@string/teenagers_mode_dialog_summary\"\n            android:title=\"@string/teenagers_mode_dialog_title\" />\n\n        <SwitchPreference\n            android:key=\"comment_copy\"\n            android:summary=\"@string/comment_copy_summary\"\n            android:title=\"@string/comment_copy_title\" />\n\n        <SwitchPreference\n            android:dependency=\"comment_copy\"\n            android:key=\"comment_copy_enhance\"\n            android:summary=\"@string/comment_copy_enhance_summary\"\n            android:title=\"@string/comment_copy_enhance_title\" />\n\n        <SwitchPreference\n            android:key=\"copy_comment\"\n            android:summary=\"@string/copy_comment_summary\"\n            android:title=\"@string/copy_comment_title\" />\n\n        <SwitchPreference\n            android:key=\"mini_program\"\n            android:summary=\"@string/mini_program_summary\"\n            android:title=\"@string/mini_program_title\" />\n\n        <SwitchPreference\n            android:key=\"auto_like\"\n            android:summary=\"@string/auto_like_summary\"\n            android:title=\"@string/auto_like_title\" />\n\n        <SwitchPreference\n            android:key=\"enable_av\"\n            android:summary=\"@string/enable_av_summary\"\n            android:title=\"@string/enable_av_title\" />\n\n        <SwitchPreference\n            android:key=\"custom_download_thread\"\n            android:summary=\"@string/custom_download_thread_summary\"\n            android:title=\"@string/custom_download_thread_title\" />\n\n        <SwitchPreference\n            android:key=\"revert_live_room_feed\"\n            android:summary=\"@string/revert_live_room_feed_summary\"\n            android:title=\"@string/revert_live_room_feed_title\" />\n\n        <SwitchPreference\n            android:key=\"replace_story_video\"\n            android:summary=\"@string/replace_story_video_summary\"\n            android:title=\"@string/replace_story_video_title\" />\n\n        <SwitchPreference\n            android:key=\"purify_share\"\n            android:summary=\"@string/purify_share_summary\"\n            android:title=\"@string/purify_share_title\" />\n\n        <Preference\n            android:key=\"custom_link\"\n            android:summary=\"@string/custom_link_summary\"\n            android:title=\"@string/custom_link_title\" />\n\n        <SwitchPreference\n            android:defaultValue=\"true\"\n            android:key=\"fix_space\"\n            android:summary=\"@string/fix_space_summary\"\n            android:title=\"@string/fix_space_title\" />\n\n        <SwitchPreference\n            android:key=\"play_arc_conf\"\n            android:summary=\"@string/play_arc_conf_summary\"\n            android:title=\"@string/play_arc_conf_title\" />\n\n        <SwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"force_browser\"\n            android:summary=\"@string/force_browser_summary\"\n            android:title=\"@string/force_browser_title\" />\n\n        <SwitchPreference\n            android:key=\"auto_generate_subtitle\"\n            android:summary=\"@string/generate_subtitle_summary\"\n            android:title=\"@string/generate_subtitle_title\" />\n\n        <SwitchPreference\n            android:key=\"forbid_switch_live_room\"\n            android:summary=\"@string/forbid_switch_live_room_summary\"\n            android:title=\"@string/forbid_switch_live_room_title\" />\n\n        <SwitchPreference\n            android:key=\"forbid_player_long_click_accelerate\"\n            android:summary=\"@string/forbid_player_long_click_accelerate_summary\"\n            android:title=\"@string/forbid_player_long_click_accelerate_title\" />\n\n        <SwitchPreference\n            android:key=\"disable_live_room_double_click\"\n            android:summary=\"@string/disable_live_room_double_click_summary\"\n            android:title=\"@string/disable_live_room_double_click_title\" />\n\n        <SwitchPreference\n            android:key=\"block_update\"\n            android:title=\"@string/block_update_title\" />\n\n        <SwitchPreference\n            android:key=\"save_comment_image\"\n            android:summary=\"@string/save_comment_image_summary\"\n            android:title=\"@string/save_comment_image_title\" />\n\n        <Preference\n            android:key=\"danmaku_filter\"\n            android:summary=\"@string/danmaku_filter_summary\"\n            android:title=\"@string/danmaku_filter_title\" />\n\n        <ListPreference\n            android:defaultValue=\"0\"\n            android:dependency=\"main_func\"\n            android:entries=\"@array/half_screen_quality_entries\"\n            android:entryValues=\"@array/half_screen_quality_values\"\n            android:key=\"half_screen_quality\"\n            android:summary=\"@string/half_screen_quality_summary\"\n            android:title=\"@string/half_screen_quality_title\" />\n\n        <ListPreference\n            android:defaultValue=\"0\"\n            android:dependency=\"main_func\"\n            android:entries=\"@array/full_screen_quality_entries\"\n            android:entryValues=\"@array/full_screen_quality_values\"\n            android:key=\"full_screen_quality\"\n            android:summary=\"@string/full_screen_quality_summary\"\n            android:title=\"@string/full_screen_quality_title\" />\n\n        <ListPreference\n            android:defaultValue=\"0\"\n            android:entries=\"@array/live_quality_entries\"\n            android:entryValues=\"@array/live_quality_values\"\n            android:key=\"live_quality\"\n            android:summary=\"@string/live_quality_summary\"\n            android:title=\"@string/live_quality_title\" />\n\n        <SwitchPreference\n            android:key=\"block_comment_guide\"\n            android:summary=\"@string/block_comment_guide_summary\"\n            android:title=\"@string/block_comment_guide_title\" />\n\n        <SwitchPreference\n            android:key=\"disable_auto_refresh\"\n            android:summary=\"@string/disable_auto_refresh_summary\"\n            android:title=\"@string/disable_auto_refresh_title\" />\n\n        <!--\n        <SwitchPreference\n            android:key=\"disable_auto_select\"\n            android:summary=\"@string/disable_auto_select_summary\"\n            android:title=\"@string/disable_auto_select_title\" />\n            -->\n\n        <SwitchPreference\n            android:key=\"disable_vip_dm_colorful\"\n            android:summary=\"@string/disable_vip_dm_colorful_summary\"\n            android:title=\"@string/disable_vip_dm_colorful_title\" />\n\n        <Preference\n            android:key=\"default_speed\"\n            android:summary=\"@string/default_speed_summary\"\n            android:title=\"@string/default_speed_title\" />\n\n        <ListPreference\n            android:defaultValue=\"0\"\n            android:entries=\"@array/pegasus_cover_ratio_entries\"\n            android:entryValues=\"@array/pegasus_cover_ratio_values\"\n            android:key=\"pegasus_cover_ratio\"\n            android:summary=\"@string/pegasus_cover_ratio_summary\"\n            android:title=\"@string/pegasus_cover_ratio_title\" />\n\n        <SwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"fake_non_multiwindow\"\n            android:summary=\"@string/fake_non_multiwindow_summary\"\n            android:title=\"@string/fake_non_multiwindow_title\" />\n\n        <Preference\n            android:defaultValue=\"300\"\n            android:key=\"long_press_speed\"\n            android:title=\"@string/long_press_speed\" />\n    </PreferenceCategory>\n    <PreferenceCategory android:title=\"@string/pref_backup\">\n        <Preference\n            android:key=\"pref_export\"\n            android:title=\"@string/pref_export_title\" />\n        <Preference\n            android:key=\"pref_import\"\n            android:title=\"@string/pref_import_title\" />\n    </PreferenceCategory>\n    <PreferenceCategory\n        android:key=\"hidden_group\"\n        android:title=\"@string/hidden_group\">\n\n        <SwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"hidden\"\n            android:summary=\"@string/hidden_summary\"\n            android:title=\"@string/hidden_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"home_filter\"\n            android:summary=\"@string/home_filter_summary\"\n            android:title=\"@string/home_filter_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"customize_bottom_bar\"\n            android:summary=\"@string/customize_bottom_bar_summary\"\n            android:title=\"@string/customize_bottom_bar_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"skip_reward_ad\"\n            android:summary=\"@string/skip_reward_ad_summary\"\n            android:title=\"@string/skip_reward_ad_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"purify_story_video_ad\"\n            android:summary=\"@string/purify_story_video_ad_summary\"\n            android:title=\"@string/purify_story_video_ad_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"purify_game\"\n            android:summary=\"@string/purify_game_summary\"\n            android:title=\"@string/purify_game_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"customize_drawer\"\n            android:summary=\"@string/customize_drawer_summary\"\n            android:title=\"@string/customize_drawer_title\" />\n        <SwitchPreference\n            android:key=\"add_custom_button\"\n            android:summary=\"@string/add_custom_button_summary\"\n            android:title=\"@string/add_custom_button_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"purify_drawer_reddot\"\n            android:summary=\"@string/purify_drawer_reddot_summary\"\n            android:title=\"@string/purify_drawer_reddot_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"drawer_style_switch\"\n            android:summary=\"@string/drawer_style_switch_summary\"\n            android:title=\"@string/drawer_style_switch_title\" />\n        <SwitchPreference\n            android:dependency=\"drawer_style_switch\"\n            android:key=\"drawer_style\"\n            android:summary=\"@string/drawer_style_summary\"\n            android:title=\"@string/drawer_style_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"purify_splash\"\n            android:summary=\"@string/purify_splash_summary\"\n            android:title=\"@string/purify_splash_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"purify_search\"\n            android:summary=\"@string/purify_search_summary\"\n            android:title=\"@string/purify_search_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"filter_search\"\n            android:summary=\"@string/filter_search_summary\"\n            android:title=\"@string/filter_search_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"filter_comment\"\n            android:summary=\"@string/filter_comment_summary\"\n            android:title=\"@string/filter_comment_title\" />\n        <MultiSelectListPreference\n            android:dependency=\"hidden\"\n            android:entries=\"@array/tab_entries\"\n            android:entryValues=\"@array/tab_values\"\n            android:key=\"customize_home_tab\"\n            android:summary=\"@string/customize_home_tab_summary\"\n            android:title=\"@string/customize_home_tab_title\" />\n        <MultiSelectListPreference\n            android:dependency=\"hidden\"\n            android:entries=\"@array/home_recommend_entries\"\n            android:entryValues=\"@array/home_recommend_values\"\n            android:key=\"customize_home_recommend\"\n            android:summary=\"@string/customize_home_recommend_summary\"\n            android:title=\"@string/customize_home_recommend_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"add_bangumi\"\n            android:summary=\"@string/add_bangumi_summary\"\n            android:title=\"@string/add_bangumi_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"add_movie\"\n            android:summary=\"@string/add_movie_summary\"\n            android:title=\"@string/add_movie_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"add_korea\"\n            android:summary=\"@string/add_korea_summary\"\n            android:title=\"@string/add_korea_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"search_area_bangumi\"\n            android:summary=\"@string/search_area_summary\"\n            android:title=\"@string/search_area_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"search_area_movie\"\n            android:summary=\"@string/search_area_movie_summary\"\n            android:title=\"@string/search_area_movie_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"customize_accessKey\"\n            android:summary=\"@string/customize_accessKey_summary\"\n            android:title=\"@string/customize_accessKey_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"force_th_comment\"\n            android:summary=\"@string/force_th_comment_summary\"\n            android:title=\"@string/force_th_comment_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"export_video\"\n            android:summary=\"@string/export_video_summary\"\n            android:title=\"@string/export_video_title\" />\n        <MultiSelectListPreference\n            android:dependency=\"hidden\"\n            android:entries=\"@array/space_entries\"\n            android:entryValues=\"@array/space_values\"\n            android:key=\"customize_space\"\n            android:summary=\"@string/customize_space_summary\"\n            android:title=\"@string/customize_space_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_upper_recommend_ad\"\n            android:summary=\"@string/block_video_ad_summary\"\n            android:title=\"@string/block_video_ad_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"disable_try_watch_vip_quality\"\n            android:summary=\"@string/disable_try_watch_vip_quality_desc\"\n            android:title=\"@string/disable_try_watch_vip_quality_title\" />\n        <Preference\n            android:dependency=\"hidden\"\n            android:key=\"customize_dynamic\"\n            android:summary=\"@string/customize_dynamic_summary\"\n            android:title=\"@string/customize_dynamic_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"remove_video_relate_promote\"\n            android:summary=\"@string/remove_video_relate_promote_summary\"\n            android:title=\"@string/remove_video_relate_promote_title\" />\n        <SwitchPreference\n            android:dependency=\"remove_video_relate_promote\"\n            android:key=\"remove_video_relate_only_av\"\n            android:summary=\"@string/remove_video_relate_only_av_summary\"\n            android:title=\"@string/remove_video_relate_only_av_title\" />\n        <SwitchPreference\n            android:dependency=\"remove_video_relate_only_av\"\n            android:key=\"remove_video_relate_nothing\"\n            android:summary=\"@string/remove_video_relate_nothing_summary\"\n            android:title=\"@string/remove_video_relate_nothing_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"remove_video_UgcSeason\"\n            android:summary=\"@string/remove_video_UgcSeason_summary\"\n            android:title=\"@string/remove_video_UgcSeason_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"remove_video_honor\"\n            android:summary=\"@string/remove_video_honor_summary\"\n            android:title=\"@string/remove_video_honor_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"remove_video_cmd_dms\"\n            android:summary=\"@string/remove_video_cmd_dms_summary\"\n            android:title=\"@string/remove_video_cmd_dms_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_word_search\"\n            android:summary=\"@string/block_word_search_summary\"\n            android:title=\"@string/block_word_search_title\" />\n\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_up_rcmd_ads\"\n            android:summary=\"@string/block_up_rcmd_ads_summary\"\n            android:title=\"@string/block_up_rcmd_ads_title\" />\n\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_view_page_ads\"\n            android:summary=\"@string/block_view_page_ads_summary\"\n            android:title=\"@string/block_view_page_ads_title\" />\n\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_live_order\"\n            android:summary=\"@string/block_live_order_summary\"\n            android:title=\"@string/block_live_order_title\" />\n\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"remove_up_vip_label\"\n            android:summary=\"@string/remove_up_vip_label_summary\"\n            android:title=\"@string/remove_up_vip_label_title\" />\n\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"no_live_mask\"\n            android:summary=\"@string/no_live_mask_summary\"\n            android:title=\"@string/no_live_mask_title\" />\n        <MultiSelectListPreference\n            android:dependency=\"hidden\"\n            android:entries=\"@array/live_popup_entries\"\n            android:entryValues=\"@array/live_popup_values\"\n            android:key=\"purify_live_popups\"\n            android:summary=\"@string/purify_live_popups_summary\"\n            android:title=\"@string/purify_live_popups_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_modules\"\n            android:summary=\"@string/block_modules_summary\"\n            android:title=\"@string/block_modules_title\" />\n        <SwitchPreference\n            android:key=\"remove_vip_section\"\n            android:summary=\"@string/remove_vip_section_summary\"\n            android:title=\"@string/remove_vip_section_title\" />\n        <SwitchPreference\n            android:dependency=\"hidden\"\n            android:key=\"block_video_comment\"\n            android:title=\"@string/block_video_comment_title\" />\n\n        <Preference\n            android:key=\"copy_access_key\"\n            android:summary=\"@string/copy_access_key_summary\"\n            android:title=\"@string/copy_access_key_title\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory android:title=\"@string/setting_category\">\n        <SwitchPreference\n            android:defaultValue=\"true\"\n            android:key=\"show_info\"\n            android:summary=\"@string/show_info_summary\"\n            android:title=\"@string/show_info_title\" />\n\n        <SwitchPreference\n            android:key=\"save_log\"\n            android:title=\"@string/save_log_title\" />\n\n        <Preference\n            android:key=\"share_log\"\n            android:summary=\"@string/share_log_summary\"\n            android:title=\"@string/share_log_title\" />\n    </PreferenceCategory>\n\n    <PreferenceCategory\n        android:key=\"about\"\n        android:title=\"@string/about\">\n\n        <Preference\n            android:key=\"version\"\n            android:title=\"@string/version\" />\n\n        <Preference\n            android:key=\"author\"\n            android:summary=\"@string/author_list\"\n            android:title=\"@string/author\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/github_url\" />\n        </Preference>\n\n        <Preference\n            android:key=\"support\"\n            android:summary=\"@string/support_summary\"\n            android:title=\"@string/support\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/afdian_url\" />\n        </Preference>\n\n        <Preference\n            android:key=\"help\"\n            android:summary=\"@string/help_summary\"\n            android:title=\"@string/help_title\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/help_url\" />\n        </Preference>\n\n        <Preference\n            android:key=\"group\"\n            android:summary=\"@string/group_summary\"\n            android:title=\"@string/group_title\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/group_url\" />\n        </Preference>\n\n        <Preference\n            android:key=\"tg\"\n            android:summary=\"@string/tg_summary\"\n            android:title=\"@string/tg_title\">\n            <intent\n                android:action=\"android.intent.action.VIEW\"\n                android:data=\"@string/tg_url\" />\n        </Preference>\n\n    </PreferenceCategory>\n\n</PreferenceScreen>\n"
  },
  {
    "path": "build.gradle.kts",
    "content": ""
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nprotobuf = \"4.30.2\"\ncoroutine = \"1.10.2\"\nkotlin = \"2.1.20\"\n\n[plugins]\nkotlin = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nagp-app = { id = \"com.android.application\", version = \"8.9.1\" }\nprotobuf = { id = \"com.google.protobuf\", version = \"0.9.5\" }\nlsplugin-jgit = { id = \"org.lsposed.lsplugin.jgit\", version = \"1.1\" }\nlsplugin-resopt = { id = \"org.lsposed.lsplugin.resopt\", version = \"1.6\" }\nlsplugin-apksign = { id = \"org.lsposed.lsplugin.apksign\", version = \"1.4\" }\nlsplugin-apktransform = { id = \"org.lsposed.lsplugin.apktransform\", version = \"1.2\" }\nlsplugin-cmaker = { id = \"org.lsposed.lsplugin.cmaker\", version = \"1.2\" }\n\n[libraries]\nxposed = { module = \"de.robv.android.xposed:api\", version = \"82\" }\ncxx = { module = \"org.lsposed.libcxx:libcxx\", version = \"29.0.14206865\" }\nprotobuf-kotlin = { module = \"com.google.protobuf:protobuf-kotlin-lite\", version.ref = \"protobuf\" }\nprotobuf-java = { module = \"com.google.protobuf:protobuf-javalite\", version.ref = \"protobuf\" }\nprotobuf-protoc = { module = \"com.google.protobuf:protoc\", version.ref = \"protobuf\" }\nkotlin-stdlib = { module = \"org.jetbrains.kotlin:kotlin-stdlib\", version.ref = \"kotlin\" }\nkotlin-coroutines-android = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-android\", version.ref = \"coroutine\" }\nkotlin-coroutines-jdk = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-jdk8\", version.ref = \"coroutine\" }\nandroidx-documentfile = { module = \"androidx.documentfile:documentfile\", version = \"1.0.1\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.12-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# org.gradle.jvmargs=-Xmx1536m\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\nandroid.useAndroidX=true\nandroid.enableAppCompileTimeRClass=true\n\nappVerName=1.6.13\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# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\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    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -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@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\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": "settings.gradle.kts",
    "content": "include(\":app\")\nbuildCache { local { removeUnusedEntriesAfterDays = 1 } }\npluginManagement {\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven(url = \"https://api.xposed.info\")\n    }\n}\nrootProject.name = \"BiliRoaming\"\n"
  }
]